mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 04:27:56 +00:00
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:
@@ -1,130 +1,212 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTelegram } from './hooks/useTelegram';
|
||||
import { usePezkuwiApi } from './hooks/usePezkuwiApi';
|
||||
import { Sidebar, Section, Announcements, Forum, Rewards, APK, Wallet } from './components';
|
||||
import { Loader2, AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { Loader2, Megaphone, MessageCircle, Gift, Smartphone, Wallet, AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// Sections
|
||||
import { AnnouncementsSection } from './components/Announcements';
|
||||
import { ForumSection } from './components/Forum';
|
||||
import { RewardsSection } from './components/Rewards';
|
||||
import { APKSection } from './components/APK';
|
||||
import { WalletSection } from './components/Wallet';
|
||||
|
||||
export type Section = 'announcements' | 'forum' | 'rewards' | 'apk' | 'wallet';
|
||||
|
||||
interface NavItem {
|
||||
id: Section;
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ id: 'announcements', icon: <Megaphone className="w-5 h-5" />, label: 'Duyurular', color: 'text-yellow-500' },
|
||||
{ id: 'forum', icon: <MessageCircle className="w-5 h-5" />, label: 'Forum', color: 'text-blue-500' },
|
||||
{ id: 'rewards', icon: <Gift className="w-5 h-5" />, label: 'Rewards', color: 'text-purple-500' },
|
||||
{ id: 'apk', icon: <Smartphone className="w-5 h-5" />, label: 'APK', color: 'text-green-500' },
|
||||
{ id: 'wallet', icon: <Wallet className="w-5 h-5" />, label: 'Wallet', color: 'text-cyan-500' },
|
||||
];
|
||||
|
||||
export function TelegramApp() {
|
||||
const {
|
||||
isReady: isTelegramReady,
|
||||
isTelegram,
|
||||
user,
|
||||
startParam,
|
||||
colorScheme,
|
||||
setHeaderColor,
|
||||
setBackgroundColor,
|
||||
enableClosingConfirmation,
|
||||
hapticSelection,
|
||||
} = useTelegram();
|
||||
|
||||
const { isReady: isApiReady, error: apiError, reconnect, isConnecting } = usePezkuwiApi();
|
||||
const { api, isApiReady, error: apiError } = usePezkuwi();
|
||||
const { isConnected } = useWallet();
|
||||
|
||||
const [activeSection, setActiveSection] = useState<Section>('announcements');
|
||||
const [isRetrying, setIsRetrying] = useState(false);
|
||||
|
||||
// Handle referral from startParam
|
||||
useEffect(() => {
|
||||
if (startParam) {
|
||||
// Store referral address in localStorage for later use
|
||||
localStorage.setItem('referrerAddress', startParam);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[TelegramApp] Referral address from startParam:', startParam);
|
||||
}
|
||||
console.log('[TelegramApp] Referral from startParam:', startParam);
|
||||
}
|
||||
}, [startParam]);
|
||||
|
||||
// Setup Telegram theme colors
|
||||
// Setup Telegram theme
|
||||
useEffect(() => {
|
||||
if (isTelegram) {
|
||||
// Set header and background colors to match our dark theme
|
||||
setHeaderColor('#111827'); // gray-900
|
||||
setBackgroundColor('#111827');
|
||||
|
||||
// Enable closing confirmation when user has unsaved changes
|
||||
// (disabled for now, can be enabled per-section)
|
||||
// enableClosingConfirmation();
|
||||
setHeaderColor('#030712'); // gray-950
|
||||
setBackgroundColor('#030712');
|
||||
}
|
||||
}, [isTelegram, setHeaderColor, setBackgroundColor]);
|
||||
|
||||
// Render the active section content
|
||||
const renderContent = () => {
|
||||
const handleNavClick = (section: Section) => {
|
||||
if (isTelegram) hapticSelection();
|
||||
setActiveSection(section);
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
setIsRetrying(true);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
// Render active section
|
||||
const renderSection = () => {
|
||||
switch (activeSection) {
|
||||
case 'announcements':
|
||||
return <Announcements />;
|
||||
return <AnnouncementsSection />;
|
||||
case 'forum':
|
||||
return <Forum />;
|
||||
return <ForumSection />;
|
||||
case 'rewards':
|
||||
return <Rewards />;
|
||||
return <RewardsSection />;
|
||||
case 'apk':
|
||||
return <APK />;
|
||||
return <APKSection />;
|
||||
case 'wallet':
|
||||
return <Wallet />;
|
||||
return <WalletSection />;
|
||||
default:
|
||||
return <Announcements />;
|
||||
return <AnnouncementsSection />;
|
||||
}
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (!isTelegramReady) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-950">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Loader2 className="w-10 h-10 text-green-500 animate-spin" />
|
||||
<span className="text-gray-400">Loading...</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-950 p-6">
|
||||
<img
|
||||
src="/shared/images/pezkuwi_wallet_logo.png"
|
||||
alt="Pezkuwi"
|
||||
className="w-20 h-20 mb-4 animate-pulse"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
<Loader2 className="w-8 h-8 text-green-500 animate-spin mb-3" />
|
||||
<span className="text-gray-400 text-sm">Pezkuwi Mini App yükleniyor...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// API connection error
|
||||
if (apiError && !isConnecting) {
|
||||
// API Error state
|
||||
if (apiError && !isApiReady) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-950 p-6">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-red-600/20 flex items-center justify-center">
|
||||
<AlertCircle className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
<h2 className="text-white font-semibold">Connection Error</h2>
|
||||
<p className="text-gray-400 text-sm max-w-xs">
|
||||
Unable to connect to Pezkuwichain network. Please check your connection and try again.
|
||||
</p>
|
||||
<button
|
||||
onClick={reconnect}
|
||||
className="flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Retry
|
||||
</button>
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-950 p-6">
|
||||
<div className="w-16 h-16 rounded-full bg-red-500/20 flex items-center justify-center mb-4">
|
||||
<AlertCircle className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
<h2 className="text-white font-semibold text-lg mb-2">Bağlantı Hatası</h2>
|
||||
<p className="text-gray-400 text-sm text-center mb-6 max-w-xs">
|
||||
Pezkuwichain ağına bağlanılamadı. Lütfen internet bağlantınızı kontrol edin.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
disabled={isRetrying}
|
||||
className="flex items-center gap-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-700 text-white px-5 py-2.5 rounded-lg transition-colors"
|
||||
>
|
||||
{isRetrying ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
)}
|
||||
Tekrar Dene
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex h-screen bg-gray-950 text-white overflow-hidden ${
|
||||
colorScheme === 'light' ? 'theme-light' : 'theme-dark'
|
||||
}`}
|
||||
>
|
||||
{/* Sidebar */}
|
||||
<Sidebar
|
||||
activeSection={activeSection}
|
||||
onSectionChange={setActiveSection}
|
||||
/>
|
||||
<div className="flex flex-col h-screen bg-gray-950 text-white overflow-hidden">
|
||||
{/* Header */}
|
||||
<header className="flex items-center justify-between px-4 py-3 bg-gray-900 border-b border-gray-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">P</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-white font-semibold text-sm">Pezkuwichain</h1>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={cn(
|
||||
"w-1.5 h-1.5 rounded-full",
|
||||
isApiReady ? "bg-green-500" : "bg-yellow-500 animate-pulse"
|
||||
)} />
|
||||
<span className="text-gray-500 text-xs">
|
||||
{isApiReady ? 'Bağlı' : 'Bağlanıyor...'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 flex flex-col overflow-hidden bg-gray-900">
|
||||
{/* API connecting indicator */}
|
||||
{!isApiReady && isConnecting && (
|
||||
<div className="bg-yellow-900/30 border-b border-yellow-700/50 px-4 py-2 flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-4 h-4 text-yellow-500 animate-spin" />
|
||||
<span className="text-yellow-500 text-sm">Connecting to network...</span>
|
||||
{isConnected && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
<span className="text-xs text-gray-400">Cüzdan Bağlı</span>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Content area */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{renderContent()}
|
||||
{/* API connecting banner */}
|
||||
{!isApiReady && (
|
||||
<div className="bg-yellow-500/10 border-b border-yellow-500/20 px-4 py-2 flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-3 h-3 text-yellow-500 animate-spin" />
|
||||
<span className="text-yellow-500 text-xs">Blockchain ağına bağlanılıyor...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-hidden">
|
||||
{renderSection()}
|
||||
</main>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<nav className="bg-gray-900 border-t border-gray-800 px-2 py-2 safe-area-bottom">
|
||||
<div className="flex items-center justify-around">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => handleNavClick(item.id)}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all min-w-[60px]",
|
||||
activeSection === item.id
|
||||
? "bg-gray-800"
|
||||
: "hover:bg-gray-800/50"
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
"transition-colors",
|
||||
activeSection === item.id ? item.color : "text-gray-500"
|
||||
)}>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span className={cn(
|
||||
"text-xs transition-colors",
|
||||
activeSection === item.id ? "text-white" : "text-gray-500"
|
||||
)}>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { useState } from 'react';
|
||||
import { Smartphone, Download, Clock, CheckCircle2, AlertCircle, ExternalLink, FileText, Shield } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTelegram } from '../../hooks/useTelegram';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
Smartphone, Download, Clock, CheckCircle2, ExternalLink,
|
||||
Shield, FileText, Wifi, ChevronDown, ChevronUp, AlertCircle,
|
||||
Github, Star, Package, Loader2
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface AppVersion {
|
||||
@@ -11,47 +20,52 @@ interface AppVersion {
|
||||
changelog: string[];
|
||||
isLatest?: boolean;
|
||||
minAndroidVersion?: string;
|
||||
downloads?: number;
|
||||
}
|
||||
|
||||
// Mock versions - will be replaced with GitHub API
|
||||
const appVersions: AppVersion[] = [
|
||||
{
|
||||
version: '1.2.0',
|
||||
releaseDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), // 2 days ago
|
||||
releaseDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
|
||||
downloadUrl: 'https://github.com/pezkuwichain/pezwallet/releases/download/v1.2.0/pezwallet-v1.2.0.apk',
|
||||
size: '45.2 MB',
|
||||
isLatest: true,
|
||||
minAndroidVersion: '7.0',
|
||||
downloads: 1234,
|
||||
changelog: [
|
||||
'New: Telegram Mini App integration',
|
||||
'New: Improved staking interface',
|
||||
'Fix: Balance refresh issues',
|
||||
'Fix: Transaction history loading',
|
||||
'Improved: Overall performance',
|
||||
'Yeni: Telegram Mini App entegrasyonu',
|
||||
'Yeni: Geliştirilmiş staking arayüzü',
|
||||
'Düzeltme: Bakiye yenileme sorunları',
|
||||
'Düzeltme: İşlem geçmişi yüklemesi',
|
||||
'İyileştirme: Genel performans',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '1.1.2',
|
||||
releaseDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14), // 14 days ago
|
||||
releaseDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14),
|
||||
downloadUrl: 'https://github.com/pezkuwichain/pezwallet/releases/download/v1.1.2/pezwallet-v1.1.2.apk',
|
||||
size: '44.8 MB',
|
||||
minAndroidVersion: '7.0',
|
||||
downloads: 856,
|
||||
changelog: [
|
||||
'Fix: Critical security update',
|
||||
'Fix: Wallet connection stability',
|
||||
'Improved: Transaction signing',
|
||||
'Düzeltme: Kritik güvenlik güncellemesi',
|
||||
'Düzeltme: Cüzdan bağlantı kararlılığı',
|
||||
'İyileştirme: İşlem imzalama',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '1.1.0',
|
||||
releaseDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30), // 30 days ago
|
||||
releaseDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30),
|
||||
downloadUrl: 'https://github.com/pezkuwichain/pezwallet/releases/download/v1.1.0/pezwallet-v1.1.0.apk',
|
||||
size: '44.5 MB',
|
||||
minAndroidVersion: '7.0',
|
||||
downloads: 2341,
|
||||
changelog: [
|
||||
'New: Multi-language support',
|
||||
'New: Dark theme improvements',
|
||||
'New: QR code scanning',
|
||||
'Fix: Various bug fixes',
|
||||
'Yeni: Çoklu dil desteği',
|
||||
'Yeni: Geliştirilmiş karanlık tema',
|
||||
'Yeni: QR kod tarama',
|
||||
'Düzeltme: Çeşitli hata düzeltmeleri',
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -59,51 +73,168 @@ const appVersions: AppVersion[] = [
|
||||
const features = [
|
||||
{
|
||||
icon: <Shield className="w-5 h-5" />,
|
||||
title: 'Secure Wallet',
|
||||
description: 'Your keys, your crypto. Full self-custody.',
|
||||
title: 'Güvenli Cüzdan',
|
||||
description: 'Anahtarlarınız, kriptonuz. Tam self-custody.',
|
||||
color: 'text-green-500',
|
||||
bgColor: 'bg-green-500/20',
|
||||
},
|
||||
{
|
||||
icon: <FileText className="w-5 h-5" />,
|
||||
title: 'Citizenship Management',
|
||||
description: 'Apply for citizenship and manage your Tiki.',
|
||||
title: 'Vatandaşlık Yönetimi',
|
||||
description: 'Vatandaşlık başvurusu ve Tiki yönetimi.',
|
||||
color: 'text-purple-500',
|
||||
bgColor: 'bg-purple-500/20',
|
||||
},
|
||||
{
|
||||
icon: <Download className="w-5 h-5" />,
|
||||
title: 'Offline Support',
|
||||
description: 'View balances and history offline.',
|
||||
icon: <Wifi className="w-5 h-5" />,
|
||||
title: 'Çevrimdışı Destek',
|
||||
description: 'Bakiye ve geçmişi çevrimdışı görüntüleyin.',
|
||||
color: 'text-blue-500',
|
||||
bgColor: 'bg-blue-500/20',
|
||||
},
|
||||
];
|
||||
|
||||
export function APK() {
|
||||
const { hapticImpact, openLink, showConfirm } = useTelegram();
|
||||
const [expandedVersion, setExpandedVersion] = useState<string | null>(appVersions[0]?.version || null);
|
||||
const [downloading, setDownloading] = useState<string | null>(null);
|
||||
|
||||
function VersionCard({
|
||||
version,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
onDownload,
|
||||
isDownloading
|
||||
}: {
|
||||
version: AppVersion;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
onDownload: () => void;
|
||||
isDownloading: boolean;
|
||||
}) {
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
return date.toLocaleDateString('tr-TR', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={cn(
|
||||
"bg-gray-900 border-gray-800 overflow-hidden",
|
||||
version.isLatest && "border-green-500/50"
|
||||
)}>
|
||||
<CardContent className="p-0">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full p-4 flex items-center justify-between hover:bg-gray-800/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"w-12 h-12 rounded-xl flex items-center justify-center",
|
||||
version.isLatest ? "bg-green-500" : "bg-gray-800"
|
||||
)}>
|
||||
{version.isLatest ? (
|
||||
<CheckCircle2 className="w-6 h-6 text-white" />
|
||||
) : (
|
||||
<Package className="w-6 h-6 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white font-semibold">v{version.version}</span>
|
||||
{version.isLatest && (
|
||||
<Badge className="bg-green-500/20 text-green-500 border-green-500/30 text-xs">
|
||||
En Son
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-gray-400 mt-1">
|
||||
<span>{formatDate(version.releaseDate)}</span>
|
||||
<span>•</span>
|
||||
<span>{version.size}</span>
|
||||
{version.downloads && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{version.downloads.toLocaleString()} indirme</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="w-5 h-5 text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Expanded content */}
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4 border-t border-gray-800">
|
||||
<div className="bg-gray-800 rounded-lg p-4 mt-4">
|
||||
<h4 className="text-sm font-medium text-white mb-3 flex items-center gap-2">
|
||||
<Star className="w-4 h-4 text-yellow-500" />
|
||||
Değişiklikler
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{version.changelog.map((item, idx) => (
|
||||
<li key={idx} className="text-sm text-gray-300 flex items-start gap-2">
|
||||
<span className="text-green-500 mt-1.5">•</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{version.minAndroidVersion && (
|
||||
<div className="mt-4 pt-3 border-t border-gray-700 text-xs text-gray-500 flex items-center gap-2">
|
||||
<Smartphone className="w-4 h-4" />
|
||||
Android {version.minAndroidVersion} veya üstü gerekli
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={onDownload}
|
||||
disabled={isDownloading}
|
||||
className={cn(
|
||||
"w-full mt-4",
|
||||
version.isLatest
|
||||
? "bg-green-600 hover:bg-green-700"
|
||||
: "bg-gray-700 hover:bg-gray-600"
|
||||
)}
|
||||
>
|
||||
{isDownloading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
İndiriliyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
v{version.version} İndir
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function APKSection() {
|
||||
const { hapticImpact, openLink, showConfirm } = useTelegram();
|
||||
const [expandedVersion, setExpandedVersion] = useState<string | null>(appVersions[0]?.version || null);
|
||||
const [downloading, setDownloading] = useState<string | null>(null);
|
||||
|
||||
const handleDownload = async (version: AppVersion) => {
|
||||
hapticImpact('medium');
|
||||
|
||||
const confirmed = await showConfirm(
|
||||
`Download Pezwallet v${version.version} (${version.size})?`
|
||||
`Pezwallet v${version.version} (${version.size}) indirilsin mi?`
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
setDownloading(version.version);
|
||||
|
||||
// Open download link
|
||||
openLink(version.downloadUrl);
|
||||
|
||||
// Reset downloading state after a delay
|
||||
setTimeout(() => {
|
||||
setDownloading(null);
|
||||
}, 3000);
|
||||
setTimeout(() => setDownloading(null), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,197 +246,149 @@ export function APK() {
|
||||
const latestVersion = appVersions.find(v => v.isLatest);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<Smartphone className="w-5 h-5 text-green-500" />
|
||||
<h2 className="text-lg font-semibold text-white">Pezwallet APK</h2>
|
||||
<div className="p-4 pb-0">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-white font-semibold text-lg flex items-center gap-2">
|
||||
<Smartphone className="w-5 h-5 text-green-500" />
|
||||
Pezwallet APK
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleOpenGitHub}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Github className="w-4 h-4 text-gray-400" />
|
||||
</Button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleOpenGitHub}
|
||||
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
<div className="flex-1 p-4 pt-0 space-y-4">
|
||||
{/* App Banner */}
|
||||
<div className="bg-gradient-to-br from-green-600 to-emerald-700 rounded-lg p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* App Icon */}
|
||||
<div className="w-16 h-16 rounded-2xl bg-white/10 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-3xl font-bold text-white">P</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold text-white mb-1">Pezwallet</h3>
|
||||
<p className="text-green-100 text-sm mb-3">
|
||||
Official wallet app for Pezkuwichain
|
||||
</p>
|
||||
{latestVersion && (
|
||||
<button
|
||||
onClick={() => handleDownload(latestVersion)}
|
||||
disabled={downloading === latestVersion.version}
|
||||
className="flex items-center gap-2 bg-white text-green-700 px-4 py-2 rounded-lg font-medium hover:bg-green-50 transition-colors disabled:opacity-70"
|
||||
>
|
||||
{downloading === latestVersion.version ? (
|
||||
<>
|
||||
<span className="animate-pulse">Downloading...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
Download v{latestVersion.version}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-gray-800 rounded-lg p-3 flex items-start gap-3 border border-gray-700"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-green-600/20 flex items-center justify-center text-green-500 flex-shrink-0">
|
||||
{feature.icon}
|
||||
<Card className="bg-gradient-to-br from-green-600 to-emerald-700 border-0 overflow-hidden">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* App Icon */}
|
||||
<div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-3xl font-bold text-white">P</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-white font-medium text-sm">{feature.title}</h4>
|
||||
<p className="text-gray-400 text-xs">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Installation Guide */}
|
||||
<div className="bg-yellow-900/30 border border-yellow-700/50 rounded-lg p-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="text-yellow-500 font-medium text-sm mb-1">Installation Guide</h4>
|
||||
<ol className="text-yellow-200/80 text-xs space-y-1 list-decimal list-inside">
|
||||
<li>Download the APK file</li>
|
||||
<li>Open your device's Settings</li>
|
||||
<li>Enable "Install from unknown sources"</li>
|
||||
<li>Open the downloaded APK file</li>
|
||||
<li>Follow the installation prompts</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version History */}
|
||||
<div className="bg-gray-800 rounded-lg border border-gray-700">
|
||||
<div className="p-4 border-b border-gray-700">
|
||||
<h3 className="text-white font-medium">Version History</h3>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-700">
|
||||
{appVersions.map((version) => (
|
||||
<div key={version.version}>
|
||||
<button
|
||||
onClick={() => setExpandedVersion(
|
||||
expandedVersion === version.version ? null : version.version
|
||||
)}
|
||||
className="w-full p-4 flex items-center justify-between hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
'w-10 h-10 rounded-lg flex items-center justify-center',
|
||||
version.isLatest ? 'bg-green-600' : 'bg-gray-700'
|
||||
)}>
|
||||
{version.isLatest ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-white" />
|
||||
) : (
|
||||
<Clock className="w-5 h-5 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white font-medium">v{version.version}</span>
|
||||
{version.isLatest && (
|
||||
<span className="text-xs bg-green-600 text-white px-1.5 py-0.5 rounded">
|
||||
Latest
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{formatDate(version.releaseDate)} • {version.size}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Download
|
||||
className={cn(
|
||||
'w-5 h-5 transition-transform',
|
||||
expandedVersion === version.version ? 'rotate-180' : ''
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold text-white mb-1">Pezwallet</h3>
|
||||
<p className="text-green-100 text-sm mb-3">
|
||||
Pezkuwichain için resmi cüzdan uygulaması
|
||||
</p>
|
||||
{latestVersion && (
|
||||
<Button
|
||||
onClick={() => handleDownload(latestVersion)}
|
||||
disabled={downloading === latestVersion.version}
|
||||
className="bg-white text-green-700 hover:bg-green-50"
|
||||
>
|
||||
{downloading === latestVersion.version ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
İndiriliyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
v{latestVersion.version} İndir
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Expanded content */}
|
||||
{expandedVersion === version.version && (
|
||||
<div className="px-4 pb-4">
|
||||
<div className="bg-gray-900 rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium text-white mb-2">Changelog</h4>
|
||||
<ul className="space-y-1">
|
||||
{version.changelog.map((item, idx) => (
|
||||
<li key={idx} className="text-xs text-gray-400 flex items-start gap-2">
|
||||
<span className="text-green-500 mt-1">•</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{version.minAndroidVersion && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-800 text-xs text-gray-500">
|
||||
Requires Android {version.minAndroidVersion} or higher
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => handleDownload(version)}
|
||||
disabled={downloading === version.version}
|
||||
className="mt-3 w-full flex items-center justify-center gap-2 bg-gray-700 hover:bg-gray-600 text-white py-2 rounded-lg text-sm transition-colors disabled:opacity-70"
|
||||
>
|
||||
{downloading === version.version ? (
|
||||
'Downloading...'
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
Download v{version.version}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Features */}
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-white flex items-center gap-2">
|
||||
<Star className="w-4 h-4 text-yellow-500" />
|
||||
Özellikler
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-3 p-3 bg-gray-800 rounded-lg"
|
||||
>
|
||||
<div className={cn(
|
||||
"w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0",
|
||||
feature.bgColor, feature.color
|
||||
)}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-white font-medium text-sm">{feature.title}</h4>
|
||||
<p className="text-gray-400 text-xs">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Installation Guide */}
|
||||
<Alert className="bg-yellow-500/10 border-yellow-500/30">
|
||||
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
||||
<AlertDescription className="text-yellow-200">
|
||||
<span className="font-medium text-yellow-500 block mb-2">Kurulum Rehberi</span>
|
||||
<ol className="text-xs space-y-1 list-decimal list-inside text-yellow-200/80">
|
||||
<li>APK dosyasını indirin</li>
|
||||
<li>Cihaz Ayarlarını açın</li>
|
||||
<li>"Bilinmeyen kaynaklardan yükleme"yi etkinleştirin</li>
|
||||
<li>İndirilen APK dosyasını açın</li>
|
||||
<li>Kurulum talimatlarını izleyin</li>
|
||||
</ol>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Version History */}
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-white flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 text-gray-400" />
|
||||
Sürüm Geçmişi
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{appVersions.map((version) => (
|
||||
<VersionCard
|
||||
key={version.version}
|
||||
version={version}
|
||||
isExpanded={expandedVersion === version.version}
|
||||
onToggle={() => setExpandedVersion(
|
||||
expandedVersion === version.version ? null : version.version
|
||||
)}
|
||||
onDownload={() => handleDownload(version)}
|
||||
isDownloading={downloading === version.version}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Footer note */}
|
||||
<div className="text-center text-xs text-gray-500 pb-4">
|
||||
<p>Always verify downloads from official sources</p>
|
||||
<p className="mt-1">
|
||||
<button
|
||||
onClick={handleOpenGitHub}
|
||||
className="text-green-500 hover:text-green-400"
|
||||
>
|
||||
View all releases on GitHub
|
||||
</button>
|
||||
<div className="text-center pb-4">
|
||||
<p className="text-xs text-gray-500">
|
||||
Daima resmi kaynaklardan indirdiğinizi doğrulayın
|
||||
</p>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={handleOpenGitHub}
|
||||
className="text-green-500 text-xs mt-1"
|
||||
>
|
||||
<Github className="w-3 h-3 mr-1" />
|
||||
GitHub'da tüm sürümleri görüntüle
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default APK;
|
||||
export default APKSection;
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { ThumbsUp, ThumbsDown, Clock, User } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTelegram } from '../../hooks/useTelegram';
|
||||
|
||||
export interface Announcement {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
author: string;
|
||||
authorAvatar?: string;
|
||||
createdAt: Date;
|
||||
likes: number;
|
||||
dislikes: number;
|
||||
userReaction?: 'like' | 'dislike' | null;
|
||||
isPinned?: boolean;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
interface AnnouncementCardProps {
|
||||
announcement: Announcement;
|
||||
onReact: (id: string, reaction: 'like' | 'dislike') => void;
|
||||
}
|
||||
|
||||
export function AnnouncementCard({ announcement, onReact }: AnnouncementCardProps) {
|
||||
const { hapticImpact, isTelegram } = useTelegram();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const handleReact = (reaction: 'like' | 'dislike') => {
|
||||
if (isTelegram) {
|
||||
hapticImpact('light');
|
||||
}
|
||||
onReact(announcement.id, reaction);
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (hours < 1) return 'Just now';
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (days < 7) return `${days}d ago`;
|
||||
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
|
||||
};
|
||||
|
||||
const truncatedContent = announcement.content.length > 200 && !isExpanded
|
||||
? announcement.content.slice(0, 200) + '...'
|
||||
: announcement.content;
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'bg-gray-800 rounded-lg p-4 border border-gray-700',
|
||||
announcement.isPinned && 'border-green-600 border-l-4'
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Author avatar */}
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center overflow-hidden">
|
||||
{announcement.authorAvatar ? (
|
||||
<img
|
||||
src={announcement.authorAvatar}
|
||||
alt={announcement.author}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<User className="w-5 h-5 text-white" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-white text-sm">{announcement.author}</h3>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-400">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formatDate(announcement.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{announcement.isPinned && (
|
||||
<span className="text-xs bg-green-600 text-white px-2 py-0.5 rounded-full">
|
||||
Pinned
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h4 className="text-white font-medium mb-2">{announcement.title}</h4>
|
||||
|
||||
{/* Content */}
|
||||
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{truncatedContent}
|
||||
</p>
|
||||
|
||||
{/* Show more/less button */}
|
||||
{announcement.content.length > 200 && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-green-500 text-sm mt-2 hover:text-green-400"
|
||||
>
|
||||
{isExpanded ? 'Show less' : 'Show more'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Image */}
|
||||
{announcement.imageUrl && (
|
||||
<div className="mt-3 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={announcement.imageUrl}
|
||||
alt=""
|
||||
className="w-full h-auto max-h-64 object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reactions */}
|
||||
<div className="flex items-center gap-4 mt-4 pt-3 border-t border-gray-700">
|
||||
<button
|
||||
onClick={() => handleReact('like')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 text-sm transition-colors',
|
||||
announcement.userReaction === 'like'
|
||||
? 'text-green-500'
|
||||
: 'text-gray-400 hover:text-green-500'
|
||||
)}
|
||||
>
|
||||
<ThumbsUp className={cn(
|
||||
'w-4 h-4',
|
||||
announcement.userReaction === 'like' && 'fill-current'
|
||||
)} />
|
||||
<span>{announcement.likes}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleReact('dislike')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 text-sm transition-colors',
|
||||
announcement.userReaction === 'dislike'
|
||||
? 'text-red-500'
|
||||
: 'text-gray-400 hover:text-red-500'
|
||||
)}
|
||||
>
|
||||
<ThumbsDown className={cn(
|
||||
'w-4 h-4',
|
||||
announcement.userReaction === 'dislike' && 'fill-current'
|
||||
)} />
|
||||
<span>{announcement.dislikes}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +1,208 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Megaphone, RefreshCw } from 'lucide-react';
|
||||
import { AnnouncementCard, Announcement } from './AnnouncementCard';
|
||||
import { useTelegram } from '../../hooks/useTelegram';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
Megaphone, RefreshCw, ThumbsUp, ThumbsDown, Pin, Clock,
|
||||
User, ChevronDown, ChevronUp, Bell
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface Announcement {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
author: string;
|
||||
authorAvatar?: string;
|
||||
createdAt: Date;
|
||||
likes: number;
|
||||
dislikes: number;
|
||||
userReaction?: 'like' | 'dislike' | null;
|
||||
isPinned?: boolean;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
// Mock data - will be replaced with API calls
|
||||
const mockAnnouncements: Announcement[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Pezkuwichain Mainnet Launch!',
|
||||
content: 'We are excited to announce that Pezkuwichain mainnet is now live! This marks a significant milestone in our journey towards building a decentralized digital state for the Kurdish people.\n\nKey features:\n- Fast transaction finality (6 seconds)\n- Low gas fees\n- Native staking support\n- Democratic governance\n\nStart exploring at app.pezkuwichain.io',
|
||||
author: 'Pezkuwi Team',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
|
||||
title: 'Pezkuwichain Mainnet Yayında!',
|
||||
content: 'Pezkuwichain mainnet artık aktif! Bu, Kürt halkı için merkezi olmayan bir dijital devlet inşa etme yolculuğumuzda önemli bir kilometre taşı.\n\nÖne çıkan özellikler:\n- Hızlı işlem kesinliği (6 saniye)\n- Düşük gas ücretleri\n- Yerleşik staking desteği\n- Demokratik yönetişim\n\nKeşfetmeye başlayın: app.pezkuwichain.io',
|
||||
author: 'Pezkuwi Ekibi',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
|
||||
likes: 142,
|
||||
dislikes: 3,
|
||||
isPinned: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'New Referral Program',
|
||||
content: 'Invite friends and earn rewards! For every friend who completes KYC, you will receive bonus points that contribute to your trust score.\n\nShare your referral link from the Rewards section.',
|
||||
author: 'Pezkuwi Team',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago
|
||||
title: 'Yeni Referral Programı',
|
||||
content: 'Arkadaşlarınızı davet edin ve ödüller kazanın! KYC tamamlayan her arkadaşınız için trust score\'unuza katkıda bulunan bonus puanlar alacaksınız.\n\nReferans linkinizi Rewards bölümünden paylaşın.',
|
||||
author: 'Pezkuwi Ekibi',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
|
||||
likes: 89,
|
||||
dislikes: 5,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Staking Rewards Update',
|
||||
content: 'We have updated the staking rewards mechanism. Citizens who stake HEZ will now earn PEZ tokens as rewards each epoch.\n\nMinimum stake: 10 HEZ\nEpoch duration: 7 days',
|
||||
author: 'Pezkuwi Team',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3), // 3 days ago
|
||||
title: 'Staking Ödülleri Güncellendi',
|
||||
content: 'Staking ödül mekanizmasını güncelledik. HEZ stake eden vatandaşlar artık her epoch\'ta PEZ token ödülü kazanacak.\n\nMinimum stake: 10 HEZ\nEpoch süresi: 7 gün',
|
||||
author: 'Pezkuwi Ekibi',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3),
|
||||
likes: 67,
|
||||
dislikes: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export function Announcements() {
|
||||
function AnnouncementCard({
|
||||
announcement,
|
||||
onReact
|
||||
}: {
|
||||
announcement: Announcement;
|
||||
onReact: (id: string, reaction: 'like' | 'dislike') => void;
|
||||
}) {
|
||||
const { hapticImpact } = useTelegram();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const handleReact = (reaction: 'like' | 'dislike') => {
|
||||
hapticImpact('light');
|
||||
onReact(announcement.id, reaction);
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (hours < 1) return 'Az önce';
|
||||
if (hours < 24) return `${hours} saat önce`;
|
||||
if (days < 7) return `${days} gün önce`;
|
||||
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
|
||||
};
|
||||
|
||||
const shouldTruncate = announcement.content.length > 200;
|
||||
const displayContent = shouldTruncate && !isExpanded
|
||||
? announcement.content.slice(0, 200) + '...'
|
||||
: announcement.content;
|
||||
|
||||
return (
|
||||
<Card className={cn(
|
||||
"bg-gray-900 border-gray-800",
|
||||
announcement.isPinned && "border-l-4 border-l-yellow-500"
|
||||
)}>
|
||||
<CardContent className="p-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-yellow-500 to-orange-600 flex items-center justify-center">
|
||||
{announcement.authorAvatar ? (
|
||||
<img
|
||||
src={announcement.authorAvatar}
|
||||
alt={announcement.author}
|
||||
className="w-full h-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<User className="w-5 h-5 text-white" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">{announcement.author}</p>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-400">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formatDate(announcement.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{announcement.isPinned && (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-500 border-yellow-500/30">
|
||||
<Pin className="w-3 h-3 mr-1" />
|
||||
Sabitlendi
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-white font-semibold mb-2">{announcement.title}</h3>
|
||||
|
||||
{/* Content */}
|
||||
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{displayContent}
|
||||
</p>
|
||||
|
||||
{/* Show more/less button */}
|
||||
{shouldTruncate && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex items-center gap-1 text-yellow-500 text-sm mt-2 hover:text-yellow-400"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
Daha az göster
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
Devamını göster
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Image */}
|
||||
{announcement.imageUrl && (
|
||||
<div className="mt-3 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={announcement.imageUrl}
|
||||
alt=""
|
||||
className="w-full h-auto max-h-64 object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reactions */}
|
||||
<div className="flex items-center gap-4 mt-4 pt-3 border-t border-gray-800">
|
||||
<button
|
||||
onClick={() => handleReact('like')}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all",
|
||||
announcement.userReaction === 'like'
|
||||
? "bg-green-500/20 text-green-500"
|
||||
: "bg-gray-800 text-gray-400 hover:bg-green-500/10 hover:text-green-500"
|
||||
)}
|
||||
>
|
||||
<ThumbsUp className={cn(
|
||||
"w-4 h-4",
|
||||
announcement.userReaction === 'like' && "fill-current"
|
||||
)} />
|
||||
<span className="text-sm font-medium">{announcement.likes}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleReact('dislike')}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all",
|
||||
announcement.userReaction === 'dislike'
|
||||
? "bg-red-500/20 text-red-500"
|
||||
: "bg-gray-800 text-gray-400 hover:bg-red-500/10 hover:text-red-500"
|
||||
)}
|
||||
>
|
||||
<ThumbsDown className={cn(
|
||||
"w-4 h-4",
|
||||
announcement.userReaction === 'dislike' && "fill-current"
|
||||
)} />
|
||||
<span className="text-sm font-medium">{announcement.dislikes}</span>
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function AnnouncementsSection() {
|
||||
const { hapticNotification } = useTelegram();
|
||||
const [announcements, setAnnouncements] = useState<Announcement[]>(mockAnnouncements);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -64,41 +231,68 @@ export function Announcements() {
|
||||
const handleRefresh = async () => {
|
||||
setIsLoading(true);
|
||||
hapticNotification('success');
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// In production, this would fetch from API
|
||||
setAnnouncements(mockAnnouncements);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// Sort: pinned first, then by date
|
||||
const sortedAnnouncements = [...announcements].sort((a, b) => {
|
||||
if (a.isPinned && !b.isPinned) return -1;
|
||||
if (!a.isPinned && b.isPinned) return 1;
|
||||
return b.createdAt.getTime() - a.createdAt.getTime();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<Megaphone className="w-5 h-5 text-green-500" />
|
||||
<h2 className="text-lg font-semibold text-white">Duyurular</h2>
|
||||
<div className="p-4 pb-0">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-white font-semibold text-lg flex items-center gap-2">
|
||||
<Megaphone className="w-5 h-5 text-yellow-500" />
|
||||
Duyurular
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<RefreshCw className={cn("w-4 h-4 text-gray-400", isLoading && "animate-spin")} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
|
||||
<Bell className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-gray-300 text-sm">{announcements.length} Duyuru</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
|
||||
<Pin className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-gray-300 text-sm">
|
||||
{announcements.filter(a => a.isPinned).length} Sabitlenmiş
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 text-gray-400 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{announcements.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
{/* Announcements List */}
|
||||
<div className="flex-1 p-4 pt-0 space-y-4">
|
||||
{isLoading ? (
|
||||
<>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-48 bg-gray-800" />
|
||||
))}
|
||||
</>
|
||||
) : sortedAnnouncements.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
|
||||
<Megaphone className="w-12 h-12 mb-3 opacity-50" />
|
||||
<p>No announcements yet</p>
|
||||
<p>Henüz duyuru yok</p>
|
||||
</div>
|
||||
) : (
|
||||
announcements.map(announcement => (
|
||||
sortedAnnouncements.map(announcement => (
|
||||
<AnnouncementCard
|
||||
key={announcement.id}
|
||||
announcement={announcement}
|
||||
@@ -111,4 +305,4 @@ export function Announcements() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Announcements;
|
||||
export default AnnouncementsSection;
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { MessageCircle, Eye, Clock, User, ChevronRight } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTelegram } from '../../hooks/useTelegram';
|
||||
|
||||
export interface ForumThread {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
author: string;
|
||||
authorAddress?: string;
|
||||
createdAt: Date;
|
||||
replyCount: number;
|
||||
viewCount: number;
|
||||
lastReplyAt?: Date;
|
||||
lastReplyAuthor?: string;
|
||||
isPinned?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
interface ThreadCardProps {
|
||||
thread: ForumThread;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function ThreadCard({ thread, onClick }: ThreadCardProps) {
|
||||
const { hapticSelection, isTelegram } = useTelegram();
|
||||
|
||||
const handleClick = () => {
|
||||
if (isTelegram) {
|
||||
hapticSelection();
|
||||
}
|
||||
onClick();
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (hours < 1) return 'Just now';
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (days < 7) return `${days}d ago`;
|
||||
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
|
||||
};
|
||||
|
||||
const truncatedContent = thread.content.length > 100
|
||||
? thread.content.slice(0, 100) + '...'
|
||||
: thread.content;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'w-full text-left bg-gray-800 rounded-lg p-4 border border-gray-700 hover:border-gray-600 transition-all',
|
||||
thread.isPinned && 'border-l-4 border-l-green-600'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Title */}
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{thread.isPinned && (
|
||||
<span className="text-xs bg-green-600 text-white px-1.5 py-0.5 rounded">
|
||||
Pinned
|
||||
</span>
|
||||
)}
|
||||
<h3 className="font-medium text-white truncate">{thread.title}</h3>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<p className="text-gray-400 text-sm mb-2 line-clamp-2">{truncatedContent}</p>
|
||||
|
||||
{/* Tags */}
|
||||
{thread.tags && thread.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{thread.tags.slice(0, 3).map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs bg-gray-700 text-gray-300 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Meta */}
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="w-3 h-3" />
|
||||
<span>{thread.author}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formatDate(thread.createdAt)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<MessageCircle className="w-3 h-3" />
|
||||
<span>{thread.replyCount}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Eye className="w-3 h-3" />
|
||||
<span>{thread.viewCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChevronRight className="w-5 h-5 text-gray-500 flex-shrink-0" />
|
||||
</div>
|
||||
|
||||
{/* Last reply info */}
|
||||
{thread.lastReplyAt && thread.lastReplyAuthor && (
|
||||
<div className="mt-3 pt-2 border-t border-gray-700 text-xs text-gray-500">
|
||||
Last reply by <span className="text-gray-400">{thread.lastReplyAuthor}</span>{' '}
|
||||
{formatDate(thread.lastReplyAt)}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { ArrowLeft, Send, User, Clock, ThumbsUp } from 'lucide-react';
|
||||
import { ForumThread } from './ThreadCard';
|
||||
import { useTelegram } from '../../hooks/useTelegram';
|
||||
|
||||
export interface ForumReply {
|
||||
id: string;
|
||||
content: string;
|
||||
author: string;
|
||||
authorAddress?: string;
|
||||
createdAt: Date;
|
||||
likes: number;
|
||||
userLiked?: boolean;
|
||||
}
|
||||
|
||||
interface ThreadViewProps {
|
||||
thread: ForumThread;
|
||||
replies: ForumReply[];
|
||||
onBack: () => void;
|
||||
onReply: (content: string) => void;
|
||||
onLikeReply: (replyId: string) => void;
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
export function ThreadView({
|
||||
thread,
|
||||
replies,
|
||||
onBack,
|
||||
onReply,
|
||||
onLikeReply,
|
||||
isConnected
|
||||
}: ThreadViewProps) {
|
||||
const { hapticImpact, showBackButton, hideBackButton, isTelegram } = useTelegram();
|
||||
const [replyContent, setReplyContent] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Setup Telegram back button
|
||||
useState(() => {
|
||||
if (isTelegram) {
|
||||
showBackButton(onBack);
|
||||
return () => hideBackButton();
|
||||
}
|
||||
});
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('tr-TR', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmitReply = async () => {
|
||||
if (!replyContent.trim() || isSubmitting) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
if (isTelegram) {
|
||||
hapticImpact('medium');
|
||||
}
|
||||
|
||||
try {
|
||||
await onReply(replyContent);
|
||||
setReplyContent('');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 p-4 border-b border-gray-800">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-gray-400" />
|
||||
</button>
|
||||
<h2 className="text-lg font-semibold text-white truncate flex-1">{thread.title}</h2>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Original post */}
|
||||
<div className="p-4 border-b border-gray-800">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white text-sm">{thread.author}</h3>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-400">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formatDate(thread.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{thread.tags && thread.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{thread.tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs bg-gray-700 text-gray-300 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{thread.content}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Replies */}
|
||||
<div className="p-4">
|
||||
<h4 className="text-sm font-medium text-gray-400 mb-4">
|
||||
{replies.length} {replies.length === 1 ? 'Reply' : 'Replies'}
|
||||
</h4>
|
||||
|
||||
<div className="space-y-4">
|
||||
{replies.map(reply => (
|
||||
<div key={reply.id} className="bg-gray-800 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-gray-400" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-white">{reply.author}</span>
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
{formatDate(reply.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onLikeReply(reply.id)}
|
||||
className={`flex items-center gap-1 text-xs transition-colors ${
|
||||
reply.userLiked ? 'text-green-500' : 'text-gray-500 hover:text-green-500'
|
||||
}`}
|
||||
>
|
||||
<ThumbsUp className={`w-3 h-3 ${reply.userLiked ? 'fill-current' : ''}`} />
|
||||
<span>{reply.likes}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-gray-300 text-sm whitespace-pre-wrap">{reply.content}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{replies.length === 0 && (
|
||||
<div className="text-center text-gray-500 py-8">
|
||||
No replies yet. Be the first to reply!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reply input */}
|
||||
{isConnected ? (
|
||||
<div className="p-4 border-t border-gray-800 bg-gray-900">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={replyContent}
|
||||
onChange={(e) => setReplyContent(e.target.value)}
|
||||
placeholder="Write a reply..."
|
||||
className="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm placeholder-gray-500 focus:outline-none focus:border-green-500"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmitReply();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSubmitReply}
|
||||
disabled={!replyContent.trim() || isSubmitting}
|
||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-700 disabled:opacity-50 rounded-lg transition-colors"
|
||||
>
|
||||
<Send className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 border-t border-gray-800 bg-gray-900 text-center text-gray-500 text-sm">
|
||||
Connect wallet to reply
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +1,405 @@
|
||||
import { useState } from 'react';
|
||||
import { MessageCircle, Plus, RefreshCw, Search } from 'lucide-react';
|
||||
import { ThreadCard, ForumThread } from './ThreadCard';
|
||||
import { ThreadView, ForumReply } from './ThreadView';
|
||||
import { useTelegram } from '../../hooks/useTelegram';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
MessageCircle, Plus, RefreshCw, Search, Pin, Clock, User,
|
||||
Eye, ChevronRight, ArrowLeft, Send, ThumbsUp, MessageSquare, Hash
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// Mock data - will be replaced with API calls
|
||||
export interface ForumThread {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
author: string;
|
||||
authorAddress?: string;
|
||||
createdAt: Date;
|
||||
replyCount: number;
|
||||
viewCount: number;
|
||||
lastReplyAt?: Date;
|
||||
lastReplyAuthor?: string;
|
||||
isPinned?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface ForumReply {
|
||||
id: string;
|
||||
content: string;
|
||||
author: string;
|
||||
authorAddress?: string;
|
||||
createdAt: Date;
|
||||
likes: number;
|
||||
userLiked?: boolean;
|
||||
}
|
||||
|
||||
// Mock data
|
||||
const mockThreads: ForumThread[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Welcome to Pezkuwi Forum!',
|
||||
content: 'This is the official community forum for Pezkuwi citizens. Feel free to discuss anything related to our digital state, governance, development, and more.\n\nPlease be respectful and follow our community guidelines.',
|
||||
title: 'Pezkuwi Forum\'a Hoş Geldiniz!',
|
||||
content: 'Bu, Pezkuwi vatandaşları için resmi topluluk forumudur. Dijital devletimiz, yönetişim, geliştirme ve daha fazlası hakkında serbestçe tartışabilirsiniz.\n\nLütfen saygılı olun ve topluluk kurallarımıza uyun.',
|
||||
author: 'Admin',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
|
||||
replyCount: 45,
|
||||
viewCount: 1234,
|
||||
isPinned: true,
|
||||
tags: ['announcement', 'rules'],
|
||||
tags: ['duyuru', 'kurallar'],
|
||||
lastReplyAt: new Date(Date.now() - 1000 * 60 * 30),
|
||||
lastReplyAuthor: 'NewCitizen',
|
||||
lastReplyAuthor: 'YeniVatandaş',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'How to stake HEZ and earn rewards?',
|
||||
content: 'Hi everyone! I just got my first HEZ tokens and want to start staking. Can someone explain the process step by step? What is the minimum amount required?',
|
||||
author: 'CryptoNewbie',
|
||||
title: 'HEZ nasıl stake edilir ve ödül kazanılır?',
|
||||
content: 'Herkese merhaba! İlk HEZ tokenlarımı aldım ve stake etmeye başlamak istiyorum. Biri adım adım süreci açıklayabilir mi? Minimum miktar ne kadar?',
|
||||
author: 'KriptoYeni',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 5),
|
||||
replyCount: 12,
|
||||
viewCount: 256,
|
||||
tags: ['staking', 'help'],
|
||||
tags: ['staking', 'yardım'],
|
||||
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
|
||||
lastReplyAuthor: 'StakingPro',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Proposal: Add support for Kurdish language in the app',
|
||||
content: 'As a Kurdish digital state, I think it would be great to have full Kurdish language support (Kurmanci and Sorani) in all our applications.\n\nWhat do you think?',
|
||||
author: 'KurdishDev',
|
||||
title: 'Öneri: Uygulamaya Kürtçe dil desteği eklenmeli',
|
||||
content: 'Kürt dijital devleti olarak, tüm uygulamalarımızda tam Kürtçe dil desteği (Kurmancî ve Soranî) olması gerektiğini düşünüyorum.\n\nNe düşünüyorsunuz?',
|
||||
author: 'KürtGeliştirici',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
|
||||
replyCount: 28,
|
||||
viewCount: 567,
|
||||
tags: ['proposal', 'localization'],
|
||||
tags: ['öneri', 'yerelleştirme'],
|
||||
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 4),
|
||||
lastReplyAuthor: 'LanguageExpert',
|
||||
lastReplyAuthor: 'DilUzmanı',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Bug report: Wallet balance not updating',
|
||||
content: 'After making a transfer, my wallet balance doesn\'t update immediately. I have to refresh the page multiple times. Is anyone else experiencing this issue?',
|
||||
author: 'TechUser',
|
||||
title: 'Hata: Cüzdan bakiyesi güncellenmiyor',
|
||||
content: 'Transfer yaptıktan sonra cüzdan bakiyem hemen güncellenmiyor. Sayfayı birkaç kez yenilemem gerekiyor. Bu sorunu yaşayan başka var mı?',
|
||||
author: 'TeknikKullanıcı',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 12),
|
||||
replyCount: 8,
|
||||
viewCount: 89,
|
||||
tags: ['bug', 'wallet'],
|
||||
tags: ['hata', 'cüzdan'],
|
||||
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 6),
|
||||
lastReplyAuthor: 'DevTeam',
|
||||
lastReplyAuthor: 'GeliştiriciEkibi',
|
||||
},
|
||||
];
|
||||
|
||||
const mockReplies: ForumReply[] = [
|
||||
{
|
||||
id: '1',
|
||||
content: 'Great to be here! Looking forward to participating in the community.',
|
||||
author: 'NewCitizen',
|
||||
content: 'Burada olmak harika! Topluluğa katılmak için sabırsızlanıyorum.',
|
||||
author: 'YeniVatandaş',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 30),
|
||||
likes: 5,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
content: 'Welcome! Make sure to check out the staking guide in the docs section.',
|
||||
author: 'Helper',
|
||||
content: 'Hoş geldiniz! Dokümantasyon bölümündeki staking rehberini kontrol etmeyi unutmayın.',
|
||||
author: 'Yardımcı',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60),
|
||||
likes: 12,
|
||||
},
|
||||
];
|
||||
|
||||
export function Forum() {
|
||||
function ThreadCard({ thread, onClick }: { thread: ForumThread; onClick: () => void }) {
|
||||
const { hapticSelection } = useTelegram();
|
||||
|
||||
const handleClick = () => {
|
||||
hapticSelection();
|
||||
onClick();
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (hours < 1) return 'Az önce';
|
||||
if (hours < 24) return `${hours}s önce`;
|
||||
if (days < 7) return `${days}g önce`;
|
||||
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
"bg-gray-900 border-gray-800 cursor-pointer hover:border-gray-700 transition-all",
|
||||
thread.isPinned && "border-l-4 border-l-blue-500"
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Title with pin badge */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{thread.isPinned && (
|
||||
<Badge className="bg-blue-500/20 text-blue-500 border-blue-500/30 text-xs">
|
||||
<Pin className="w-3 h-3 mr-1" />
|
||||
Sabit
|
||||
</Badge>
|
||||
)}
|
||||
<h3 className="font-medium text-white truncate">{thread.title}</h3>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<p className="text-gray-400 text-sm mb-3 line-clamp-2">
|
||||
{thread.content.length > 100 ? thread.content.slice(0, 100) + '...' : thread.content}
|
||||
</p>
|
||||
|
||||
{/* Tags */}
|
||||
{thread.tags && thread.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
{thread.tags.slice(0, 3).map(tag => (
|
||||
<div
|
||||
key={tag}
|
||||
className="flex items-center gap-1 text-xs bg-gray-800 text-gray-300 px-2 py-1 rounded-md"
|
||||
>
|
||||
<Hash className="w-3 h-3" />
|
||||
{tag}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Meta info */}
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="w-3 h-3" />
|
||||
<span>{thread.author}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formatDate(thread.createdAt)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<MessageCircle className="w-3 h-3" />
|
||||
<span>{thread.replyCount}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Eye className="w-3 h-3" />
|
||||
<span>{thread.viewCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChevronRight className="w-5 h-5 text-gray-500 flex-shrink-0" />
|
||||
</div>
|
||||
|
||||
{/* Last reply info */}
|
||||
{thread.lastReplyAt && thread.lastReplyAuthor && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-800 text-xs text-gray-500">
|
||||
Son yanıt: <span className="text-gray-400">{thread.lastReplyAuthor}</span>{' '}
|
||||
· {formatDate(thread.lastReplyAt)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ThreadView({
|
||||
thread,
|
||||
replies,
|
||||
onBack,
|
||||
onReply,
|
||||
onLikeReply,
|
||||
isConnected
|
||||
}: {
|
||||
thread: ForumThread;
|
||||
replies: ForumReply[];
|
||||
onBack: () => void;
|
||||
onReply: (content: string) => void;
|
||||
onLikeReply: (replyId: string) => void;
|
||||
isConnected: boolean;
|
||||
}) {
|
||||
const { hapticImpact } = useTelegram();
|
||||
const [replyContent, setReplyContent] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('tr-TR', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmitReply = async () => {
|
||||
if (!replyContent.trim() || isSubmitting) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
hapticImpact('medium');
|
||||
|
||||
try {
|
||||
await onReply(replyContent);
|
||||
setReplyContent('');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-gray-950">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 p-4 border-b border-gray-800">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onBack}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-gray-400" />
|
||||
</Button>
|
||||
<h2 className="text-white font-semibold truncate flex-1">{thread.title}</h2>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Original post */}
|
||||
<div className="p-4 border-b border-gray-800">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">{thread.author}</p>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-400">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formatDate(thread.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{thread.tags && thread.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
{thread.tags.map(tag => (
|
||||
<Badge key={tag} variant="outline" className="text-gray-400 border-gray-700 text-xs">
|
||||
<Hash className="w-3 h-3 mr-1" />
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{thread.content}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Replies */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<MessageSquare className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-white font-medium text-sm">
|
||||
{replies.length} Yanıt
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{replies.map(reply => (
|
||||
<Card key={reply.id} className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-gray-400" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-white">{reply.author}</span>
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
{formatDate(reply.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onLikeReply(reply.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-all",
|
||||
reply.userLiked
|
||||
? "bg-green-500/20 text-green-500"
|
||||
: "bg-gray-800 text-gray-500 hover:text-green-500"
|
||||
)}
|
||||
>
|
||||
<ThumbsUp className={cn("w-3 h-3", reply.userLiked && "fill-current")} />
|
||||
<span>{reply.likes}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-gray-300 text-sm whitespace-pre-wrap">{reply.content}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{replies.length === 0 && (
|
||||
<div className="text-center text-gray-500 py-8">
|
||||
<MessageCircle className="w-10 h-10 mx-auto mb-2 opacity-50" />
|
||||
<p>Henüz yanıt yok. İlk yanıtı siz yazın!</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reply input */}
|
||||
{isConnected ? (
|
||||
<div className="p-4 border-t border-gray-800 bg-gray-900">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={replyContent}
|
||||
onChange={(e) => setReplyContent(e.target.value)}
|
||||
placeholder="Yanıtınızı yazın..."
|
||||
className="flex-1 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmitReply();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSubmitReply}
|
||||
disabled={!replyContent.trim() || isSubmitting}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 border-t border-gray-800 bg-gray-900 text-center text-gray-500 text-sm">
|
||||
Yanıt yazmak için cüzdanınızı bağlayın
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ForumSection() {
|
||||
const { hapticNotification, showAlert } = useTelegram();
|
||||
const { selectedAccount } = usePezkuwi();
|
||||
const { isConnected } = useWallet();
|
||||
|
||||
const [threads, setThreads] = useState<ForumThread[]>(mockThreads);
|
||||
const [selectedThread, setSelectedThread] = useState<ForumThread | null>(null);
|
||||
const [replies, setReplies] = useState<ForumReply[]>(mockReplies);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const isConnected = !!selectedAccount;
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsLoading(true);
|
||||
hapticNotification('success');
|
||||
@@ -96,11 +410,10 @@ export function Forum() {
|
||||
|
||||
const handleCreateThread = () => {
|
||||
if (!isConnected) {
|
||||
showAlert('Please connect your wallet to create a thread');
|
||||
showAlert('Konu oluşturmak için cüzdanınızı bağlayın');
|
||||
return;
|
||||
}
|
||||
// TODO: Implement thread creation modal
|
||||
showAlert('Thread creation coming soon!');
|
||||
showAlert('Konu oluşturma özelliği yakında!');
|
||||
};
|
||||
|
||||
const handleReply = async (content: string) => {
|
||||
@@ -109,7 +422,7 @@ export function Forum() {
|
||||
const newReply: ForumReply = {
|
||||
id: String(Date.now()),
|
||||
content,
|
||||
author: selectedAccount?.meta?.name || 'Anonymous',
|
||||
author: selectedAccount?.meta?.name || 'Anonim',
|
||||
createdAt: new Date(),
|
||||
likes: 0,
|
||||
};
|
||||
@@ -155,57 +468,81 @@ export function Forum() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="w-5 h-5 text-green-500" />
|
||||
<h2 className="text-lg font-semibold text-white">Forum</h2>
|
||||
<div className="p-4 pb-0">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-white font-semibold text-lg flex items-center gap-2">
|
||||
<MessageCircle className="w-5 h-5 text-blue-500" />
|
||||
Forum
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<RefreshCw className={cn("w-4 h-4 text-gray-400", isLoading && "animate-spin")} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={handleCreateThread}
|
||||
className="h-8 w-8 bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 text-gray-400 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreateThread}
|
||||
className="p-2 rounded-lg bg-green-600 hover:bg-green-700 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="p-4 border-b border-gray-800">
|
||||
<div className="relative">
|
||||
{/* Search */}
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search threads..."
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg pl-9 pr-3 py-2 text-white text-sm placeholder-gray-500 focus:outline-none focus:border-green-500"
|
||||
placeholder="Konularda ara..."
|
||||
className="pl-9 bg-gray-900 border-gray-800 text-white placeholder:text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
|
||||
<MessageSquare className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-gray-300 text-sm">{threads.length} Konu</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
|
||||
<Pin className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-gray-300 text-sm">
|
||||
{threads.filter(t => t.isPinned).length} Sabitlenmiş
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{sortedThreads.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
{/* Thread List */}
|
||||
<div className="flex-1 p-4 pt-0 space-y-3">
|
||||
{isLoading ? (
|
||||
<>
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-32 bg-gray-800" />
|
||||
))}
|
||||
</>
|
||||
) : sortedThreads.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
|
||||
<MessageCircle className="w-12 h-12 mb-3 opacity-50" />
|
||||
<p>{searchQuery ? 'No threads found' : 'No threads yet'}</p>
|
||||
<p>{searchQuery ? 'Konu bulunamadı' : 'Henüz konu yok'}</p>
|
||||
{!searchQuery && (
|
||||
<button
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={handleCreateThread}
|
||||
className="mt-4 text-green-500 hover:text-green-400"
|
||||
className="mt-2 text-blue-500"
|
||||
>
|
||||
Create the first thread
|
||||
</button>
|
||||
İlk konuyu oluştur
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -222,4 +559,4 @@ export function Forum() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Forum;
|
||||
export default ForumSection;
|
||||
|
||||
@@ -1,82 +1,42 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Gift, Users, Trophy, Calendar, Copy, Check, Share2, Loader2, Star } from 'lucide-react';
|
||||
import { useTelegram } from '../../hooks/useTelegram';
|
||||
import { usePezkuwiApi } from '../../hooks/usePezkuwiApi';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { getReferralStats, calculateReferralScore, ReferralStats } from '@shared/lib/referral';
|
||||
import { getAllScores, UserScores, getScoreRating, getScoreColor } from '@shared/lib/scores';
|
||||
import { useReferral } from '@/contexts/ReferralContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { useTelegram } from '../../hooks/useTelegram';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { getReferralStats, ReferralStats, getMyReferrals, calculateReferralScore } from '@shared/lib/referral';
|
||||
import { getStakingInfo, StakingInfo } from '@shared/lib/staking';
|
||||
import {
|
||||
Gift, Users, Trophy, Copy, Check, Share2, Loader2, RefreshCw,
|
||||
UserPlus, Award, Star, Calendar, Zap, ChevronRight, Clock
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DailyTask {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
reward: number;
|
||||
completed: boolean;
|
||||
progress?: number;
|
||||
maxProgress?: number;
|
||||
}
|
||||
export function RewardsSection() {
|
||||
const { api, isApiReady, selectedAccount } = usePezkuwi();
|
||||
const { stats, myReferrals, loading: referralLoading, refreshStats } = useReferral();
|
||||
const { isConnected } = useWallet();
|
||||
const { hapticNotification, hapticImpact, showAlert, openTelegramLink } = useTelegram();
|
||||
|
||||
const dailyTasks: DailyTask[] = [
|
||||
{
|
||||
id: 'login',
|
||||
title: 'Daily Login',
|
||||
description: 'Open the app daily',
|
||||
reward: 5,
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
id: 'forum',
|
||||
title: 'Forum Activity',
|
||||
description: 'Post or reply in forum',
|
||||
reward: 10,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
id: 'referral',
|
||||
title: 'Invite a Friend',
|
||||
description: 'Invite a new user to join',
|
||||
reward: 50,
|
||||
completed: false,
|
||||
},
|
||||
];
|
||||
|
||||
export function Rewards() {
|
||||
const { hapticNotification, hapticImpact, showAlert, user, openTelegramLink } = useTelegram();
|
||||
const { api, isReady: isApiReady } = usePezkuwiApi();
|
||||
const { selectedAccount } = usePezkuwi();
|
||||
|
||||
const [referralStats, setReferralStats] = useState<ReferralStats | null>(null);
|
||||
const [userScores, setUserScores] = useState<UserScores | null>(null);
|
||||
const [stakingInfo, setStakingInfo] = useState<StakingInfo | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [claimingEpoch, setClaimingEpoch] = useState<number | null>(null);
|
||||
|
||||
const isConnected = !!selectedAccount;
|
||||
const address = selectedAccount?.address;
|
||||
const referralLink = address ? `https://t.me/pezkuwichain_bot?start=${address}` : '';
|
||||
|
||||
// Generate referral link
|
||||
const referralLink = address
|
||||
? `https://t.me/pezkuwichain?start=${address}`
|
||||
: '';
|
||||
|
||||
// Fetch data when connected
|
||||
// Fetch staking data
|
||||
useEffect(() => {
|
||||
if (!api || !isApiReady || !address) return;
|
||||
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [stats, scores, staking] = await Promise.all([
|
||||
getReferralStats(api, address),
|
||||
getAllScores(api, address),
|
||||
getStakingInfo(api, address),
|
||||
]);
|
||||
|
||||
setReferralStats(stats);
|
||||
setUserScores(scores);
|
||||
const staking = await getStakingInfo(api, address);
|
||||
setStakingInfo(staking);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch rewards data:', err);
|
||||
@@ -90,267 +50,301 @@ export function Rewards() {
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
if (!referralLink) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(referralLink);
|
||||
setCopied(true);
|
||||
hapticNotification('success');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
showAlert('Failed to copy link');
|
||||
showAlert('Link kopyalanamadı');
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
if (!referralLink) return;
|
||||
|
||||
hapticImpact('medium');
|
||||
const text = `Join Pezkuwichain - The Digital Kurdish State! Use my referral link:`;
|
||||
const shareUrl = `https://t.me/share/url?url=${encodeURIComponent(referralLink)}&text=${encodeURIComponent(text)}`;
|
||||
openTelegramLink(shareUrl);
|
||||
const text = encodeURIComponent('Pezkuwichain - Kürt Dijital Devleti! Referans linkimle katıl:');
|
||||
openTelegramLink(`https://t.me/share/url?url=${encodeURIComponent(referralLink)}&text=${text}`);
|
||||
};
|
||||
|
||||
const handleClaimEpoch = async (epoch: number) => {
|
||||
if (!api || !selectedAccount) return;
|
||||
|
||||
setClaimingEpoch(epoch);
|
||||
hapticImpact('medium');
|
||||
|
||||
try {
|
||||
// TODO: Implement actual claim transaction
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
hapticNotification('success');
|
||||
showAlert(`Successfully claimed rewards for epoch ${epoch}!`);
|
||||
} catch (err) {
|
||||
hapticNotification('error');
|
||||
showAlert('Failed to claim rewards');
|
||||
} finally {
|
||||
setClaimingEpoch(null);
|
||||
}
|
||||
const handleRefresh = async () => {
|
||||
hapticNotification('success');
|
||||
await refreshStats();
|
||||
};
|
||||
|
||||
if (!isConnected) {
|
||||
// Not connected state
|
||||
if (!isConnected || !selectedAccount) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center gap-2 p-4 border-b border-gray-800">
|
||||
<Gift className="w-5 h-5 text-green-500" />
|
||||
<h2 className="text-lg font-semibold text-white">Rewards</h2>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-gray-500 p-6">
|
||||
<Gift className="w-16 h-16 mb-4 opacity-50" />
|
||||
<p className="text-center mb-4">Connect your wallet to view rewards and referrals</p>
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-6">
|
||||
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-purple-500/20 to-pink-500/20 flex items-center justify-center mb-6">
|
||||
<Gift className="w-12 h-12 text-purple-500" />
|
||||
</div>
|
||||
<h2 className="text-white font-semibold text-xl mb-2">Ödüller ve Referanslar</h2>
|
||||
<p className="text-gray-400 text-sm text-center mb-8 max-w-xs">
|
||||
Referans programına katılmak ve ödüllerinizi görmek için cüzdanınızı bağlayın.
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-4 w-full max-w-sm">
|
||||
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
|
||||
<UserPlus className="w-6 h-6 text-green-500 mb-2" />
|
||||
<span className="text-xs text-gray-400 text-center">Arkadaş Davet</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
|
||||
<Trophy className="w-6 h-6 text-yellow-500 mb-2" />
|
||||
<span className="text-xs text-gray-400 text-center">Puan Kazan</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
|
||||
<Zap className="w-6 h-6 text-orange-500 mb-2" />
|
||||
<span className="text-xs text-gray-400 text-center">PEZ Ödülü</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 p-4 border-b border-gray-800">
|
||||
<Gift className="w-5 h-5 text-green-500" />
|
||||
<h2 className="text-lg font-semibold text-white">Rewards</h2>
|
||||
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
|
||||
{/* Header Stats */}
|
||||
<div className="p-4 pb-0">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-white font-semibold text-lg flex items-center gap-2">
|
||||
<Gift className="w-5 h-5 text-purple-500" />
|
||||
Ödüller
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={referralLoading}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<RefreshCw className={cn("w-4 h-4 text-gray-400", referralLoading && "animate-spin")} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-3 gap-3 mb-4">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-3 text-center">
|
||||
<div className="w-10 h-10 rounded-full bg-green-500/20 flex items-center justify-center mx-auto mb-2">
|
||||
<Users className="w-5 h-5 text-green-500" />
|
||||
</div>
|
||||
{referralLoading ? (
|
||||
<Skeleton className="h-6 w-8 mx-auto bg-gray-700" />
|
||||
) : (
|
||||
<p className="text-white text-xl font-bold">{stats?.referralCount || 0}</p>
|
||||
)}
|
||||
<p className="text-gray-500 text-xs">Referans</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-3 text-center">
|
||||
<div className="w-10 h-10 rounded-full bg-yellow-500/20 flex items-center justify-center mx-auto mb-2">
|
||||
<Trophy className="w-5 h-5 text-yellow-500" />
|
||||
</div>
|
||||
{referralLoading ? (
|
||||
<Skeleton className="h-6 w-8 mx-auto bg-gray-700" />
|
||||
) : (
|
||||
<p className="text-white text-xl font-bold">{stats?.referralScore || 0}</p>
|
||||
)}
|
||||
<p className="text-gray-500 text-xs">Puan</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-3 text-center">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-500/20 flex items-center justify-center mx-auto mb-2">
|
||||
<Award className="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-6 w-8 mx-auto bg-gray-700" />
|
||||
) : (
|
||||
<p className="text-white text-xl font-bold">{stakingInfo?.stakingScore || 0}</p>
|
||||
)}
|
||||
<p className="text-gray-500 text-xs">Staking</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<Loader2 className="w-8 h-8 text-green-500 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Score Overview */}
|
||||
{userScores && (
|
||||
<div className="bg-gradient-to-br from-green-600 to-emerald-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Trophy className="w-5 h-5 text-yellow-400" />
|
||||
<span className="text-white font-medium">Trust Score</span>
|
||||
</div>
|
||||
<span className={cn('text-sm font-medium', getScoreColor(userScores.totalScore))}>
|
||||
{getScoreRating(userScores.totalScore)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-4xl font-bold text-white mb-2">
|
||||
{userScores.totalScore}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="bg-black/20 rounded px-2 py-1">
|
||||
<span className="text-green-200">Staking:</span>{' '}
|
||||
<span className="text-white">{userScores.stakingScore}</span>
|
||||
</div>
|
||||
<div className="bg-black/20 rounded px-2 py-1">
|
||||
<span className="text-green-200">Referral:</span>{' '}
|
||||
<span className="text-white">{userScores.referralScore}</span>
|
||||
</div>
|
||||
<div className="bg-black/20 rounded px-2 py-1">
|
||||
<span className="text-green-200">Tiki:</span>{' '}
|
||||
<span className="text-white">{userScores.tikiScore}</span>
|
||||
</div>
|
||||
<div className="bg-black/20 rounded px-2 py-1">
|
||||
<span className="text-green-200">Trust:</span>{' '}
|
||||
<span className="text-white">{userScores.trustScore}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Referral Invite Section */}
|
||||
<div className="px-4 pb-4">
|
||||
<Card className="bg-gradient-to-br from-purple-600 to-pink-600 border-0">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<UserPlus className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Referral Section */}
|
||||
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Users className="w-5 h-5 text-green-500" />
|
||||
<span className="text-white font-medium">Referral Program</span>
|
||||
<div>
|
||||
<h3 className="text-white font-semibold">Arkadaşını Davet Et</h3>
|
||||
<p className="text-purple-100 text-sm">Her referans için puan kazan!</p>
|
||||
</div>
|
||||
|
||||
{referralStats && (
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
<div className="bg-gray-900 rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{referralStats.referralCount}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Referrals</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-green-500">
|
||||
{referralStats.referralScore}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Score</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Referral link */}
|
||||
<div className="bg-gray-900 rounded-lg p-3 mb-3">
|
||||
<div className="text-xs text-gray-400 mb-1">Your referral link</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 text-sm text-gray-300 truncate">
|
||||
{referralLink}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="p-2 rounded bg-gray-700 hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="w-full flex items-center justify-center gap-2 bg-green-600 hover:bg-green-700 text-white py-2.5 rounded-lg transition-colors"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
Share via Telegram
|
||||
</button>
|
||||
|
||||
{/* Who invited me */}
|
||||
{referralStats?.whoInvitedMe && (
|
||||
<div className="mt-3 text-sm text-gray-400">
|
||||
Invited by:{' '}
|
||||
<span className="text-gray-300">
|
||||
{referralStats.whoInvitedMe.slice(0, 8)}...{referralStats.whoInvitedMe.slice(-6)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Epoch Rewards */}
|
||||
{stakingInfo?.pezRewards && stakingInfo.pezRewards.hasPendingClaim && (
|
||||
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Calendar className="w-5 h-5 text-green-500" />
|
||||
<span className="text-white font-medium">Epoch Rewards</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 rounded-lg p-3 mb-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-gray-400 text-sm">Current Epoch</span>
|
||||
<span className="text-white font-medium">
|
||||
#{stakingInfo.pezRewards.currentEpoch}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-400 text-sm">Claimable</span>
|
||||
<span className="text-green-500 font-bold">
|
||||
{stakingInfo.pezRewards.totalClaimable} PEZ
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{stakingInfo.pezRewards.claimableRewards.map(reward => (
|
||||
<div
|
||||
key={reward.epoch}
|
||||
className="flex items-center justify-between bg-gray-900 rounded-lg p-3"
|
||||
>
|
||||
<div>
|
||||
<div className="text-sm text-white">Epoch #{reward.epoch}</div>
|
||||
<div className="text-xs text-green-500">{reward.amount} PEZ</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleClaimEpoch(reward.epoch)}
|
||||
disabled={claimingEpoch === reward.epoch}
|
||||
className="px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:bg-gray-700 text-white text-sm rounded-lg transition-colors"
|
||||
>
|
||||
{claimingEpoch === reward.epoch ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
'Claim'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Referral Link */}
|
||||
<div className="bg-black/20 rounded-lg p-3 mb-3">
|
||||
<p className="text-purple-200 text-xs mb-1">Referans Linkin</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 text-white text-xs truncate">{referralLink}</code>
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-white" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Daily Tasks */}
|
||||
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Star className="w-5 h-5 text-yellow-500" />
|
||||
<span className="text-white font-medium">Daily Tasks</span>
|
||||
<Button
|
||||
onClick={handleShare}
|
||||
className="w-full bg-white text-purple-600 hover:bg-purple-50"
|
||||
>
|
||||
<Share2 className="w-4 h-4 mr-2" />
|
||||
Telegram'da Paylaş
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Score System Info */}
|
||||
<div className="px-4 pb-4">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-white flex items-center gap-2">
|
||||
<Star className="w-4 h-4 text-yellow-500" />
|
||||
Puan Sistemi
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between p-2 bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-400 text-xs">1-10 referans</span>
|
||||
</div>
|
||||
<span className="text-green-400 text-sm font-medium">×10 puan</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-2 bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-400 text-xs">11-50 referans</span>
|
||||
</div>
|
||||
<span className="text-green-400 text-sm font-medium">100 + ×5 puan</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-2 bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-400 text-xs">51-100 referans</span>
|
||||
</div>
|
||||
<span className="text-green-400 text-sm font-medium">300 + ×4 puan</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-2 bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-400 text-xs">101+ referans</span>
|
||||
</div>
|
||||
<span className="text-yellow-400 text-sm font-medium">500 (Max)</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Epoch Rewards */}
|
||||
{stakingInfo?.pezRewards?.hasPendingClaim && (
|
||||
<div className="px-4 pb-4">
|
||||
<Card className="bg-gradient-to-br from-orange-600 to-yellow-600 border-0">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5 text-white" />
|
||||
<span className="text-white font-medium">Epoch Ödülleri</span>
|
||||
</div>
|
||||
<Badge className="bg-white/20 text-white border-0">
|
||||
Epoch #{stakingInfo.pezRewards.currentEpoch}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{dailyTasks.map(task => (
|
||||
<div
|
||||
key={task.id}
|
||||
className={cn(
|
||||
'bg-gray-900 rounded-lg p-3 flex items-center justify-between',
|
||||
task.completed && 'opacity-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn(
|
||||
'text-sm font-medium',
|
||||
task.completed ? 'text-gray-400 line-through' : 'text-white'
|
||||
)}>
|
||||
{task.title}
|
||||
</span>
|
||||
{task.completed && (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{task.description}</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-yellow-500">
|
||||
+{task.reward} pts
|
||||
</div>
|
||||
<div className="bg-black/20 rounded-lg p-3 mb-3">
|
||||
<p className="text-orange-100 text-xs mb-1">Bekleyen PEZ</p>
|
||||
<p className="text-white text-2xl font-bold">
|
||||
{stakingInfo.pezRewards.totalClaimable} PEZ
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-3">
|
||||
{stakingInfo.pezRewards.claimableRewards.map((reward) => (
|
||||
<div key={reward.epoch} className="flex items-center justify-between bg-black/10 rounded-lg p-2">
|
||||
<span className="text-orange-100 text-sm">Epoch #{reward.epoch}</span>
|
||||
<span className="text-white font-medium">{reward.amount} PEZ</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full bg-white text-orange-600 hover:bg-orange-50"
|
||||
onClick={() => showAlert('Claim özelliği yakında!')}
|
||||
>
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
Tümünü Claim Et
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* My Referrals List */}
|
||||
{myReferrals && myReferrals.length > 0 && (
|
||||
<div className="px-4 pb-6">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-white flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-green-500" />
|
||||
Referanslarım ({myReferrals.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{myReferrals.slice(0, 5).map((referral, index) => (
|
||||
<div key={referral} className="flex items-center justify-between p-2 bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-green-500/20 flex items-center justify-center">
|
||||
<span className="text-green-500 text-xs font-bold">{index + 1}</span>
|
||||
</div>
|
||||
<code className="text-gray-300 text-xs">
|
||||
{referral.slice(0, 6)}...{referral.slice(-4)}
|
||||
</code>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-green-400 border-green-500/30 text-xs">
|
||||
KYC Onaylı
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
{myReferrals.length > 5 && (
|
||||
<p className="text-gray-500 text-xs text-center pt-2">
|
||||
+{myReferrals.length - 5} daha fazla
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Who invited me */}
|
||||
{stats?.whoInvitedMe && (
|
||||
<div className="px-4 pb-6">
|
||||
<Alert className="bg-blue-500/10 border-blue-500/30">
|
||||
<Award className="w-4 h-4 text-blue-500" />
|
||||
<AlertDescription className="text-blue-200 text-sm">
|
||||
<span className="text-gray-400">Davet eden: </span>
|
||||
<code className="text-blue-300">
|
||||
{stats.whoInvitedMe.slice(0, 8)}...{stats.whoInvitedMe.slice(-6)}
|
||||
</code>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Rewards;
|
||||
export default RewardsSection;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,98 +1,45 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Wallet as WalletIcon, Send, ArrowDownToLine, RefreshCw, Copy, Check, Loader2, TrendingUp, Clock, ExternalLink } from 'lucide-react';
|
||||
import { useTelegram } from '../../hooks/useTelegram';
|
||||
import { usePezkuwiApi } from '../../hooks/usePezkuwiApi';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { formatBalance, CHAIN_CONFIG, formatAddress } from '@shared/lib/wallet';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { useTelegram } from '../../hooks/useTelegram';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { getAllScores, UserScores, getScoreRating } from '@shared/lib/scores';
|
||||
import { getStakingInfo, StakingInfo } from '@shared/lib/staking';
|
||||
import { formatBalance, formatAddress, CHAIN_CONFIG } from '@shared/lib/wallet';
|
||||
import {
|
||||
Wallet, Send, ArrowDownToLine, TrendingUp, Copy, Check, ExternalLink,
|
||||
Loader2, RefreshCw, Trophy, Users, Star, Award, Coins, Clock, Zap
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface TokenBalance {
|
||||
symbol: string;
|
||||
name: string;
|
||||
balance: string;
|
||||
balanceUsd?: string;
|
||||
icon?: string;
|
||||
isNative?: boolean;
|
||||
}
|
||||
|
||||
interface Transaction {
|
||||
id: string;
|
||||
type: 'send' | 'receive' | 'stake' | 'unstake' | 'claim';
|
||||
amount: string;
|
||||
symbol: string;
|
||||
address?: string;
|
||||
timestamp: Date;
|
||||
status: 'pending' | 'confirmed' | 'failed';
|
||||
}
|
||||
|
||||
// Mock recent transactions - will be replaced with actual data
|
||||
const mockTransactions: Transaction[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'receive',
|
||||
amount: '100.00',
|
||||
symbol: 'HEZ',
|
||||
address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2),
|
||||
status: 'confirmed',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'stake',
|
||||
amount: '50.00',
|
||||
symbol: 'HEZ',
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24),
|
||||
status: 'confirmed',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'claim',
|
||||
amount: '5.25',
|
||||
symbol: 'PEZ',
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3),
|
||||
status: 'confirmed',
|
||||
},
|
||||
];
|
||||
|
||||
export function Wallet() {
|
||||
export function WalletSection() {
|
||||
const { api, isApiReady, selectedAccount, connectWallet, disconnectWallet, accounts } = usePezkuwi();
|
||||
const { balances, refreshBalances, isConnected } = useWallet();
|
||||
const { hapticNotification, hapticImpact, showAlert, openLink } = useTelegram();
|
||||
const { api, isReady: isApiReady } = usePezkuwiApi();
|
||||
const { selectedAccount, connectWallet, disconnectWallet } = usePezkuwi();
|
||||
|
||||
const [balances, setBalances] = useState<TokenBalance[]>([]);
|
||||
const [scores, setScores] = useState<UserScores | null>(null);
|
||||
const [stakingInfo, setStakingInfo] = useState<StakingInfo | null>(null);
|
||||
const [transactions, setTransactions] = useState<Transaction[]>(mockTransactions);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const isConnected = !!selectedAccount;
|
||||
const address = selectedAccount?.address;
|
||||
|
||||
// Fetch balances when connected
|
||||
// Fetch data when connected
|
||||
useEffect(() => {
|
||||
if (!api || !isApiReady || !address) return;
|
||||
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Fetch native balance
|
||||
const accountInfo = await api.query.system.account(address);
|
||||
const { data } = accountInfo.toJSON() as { data: { free: string; reserved: string } };
|
||||
const freeBalance = BigInt(data.free || 0);
|
||||
|
||||
const nativeBalance: TokenBalance = {
|
||||
symbol: CHAIN_CONFIG.symbol,
|
||||
name: 'Hezar Token',
|
||||
balance: formatBalance(freeBalance.toString()),
|
||||
isNative: true,
|
||||
};
|
||||
|
||||
setBalances([nativeBalance]);
|
||||
|
||||
// Fetch staking info
|
||||
const staking = await getStakingInfo(api, address);
|
||||
const [userScores, staking] = await Promise.all([
|
||||
getAllScores(api, address),
|
||||
getStakingInfo(api, address),
|
||||
]);
|
||||
setScores(userScores);
|
||||
setStakingInfo(staking);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch wallet data:', err);
|
||||
@@ -106,26 +53,19 @@ export function Wallet() {
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if (!api || !address || isRefreshing) return;
|
||||
|
||||
setIsRefreshing(true);
|
||||
hapticNotification('success');
|
||||
|
||||
try {
|
||||
const accountInfo = await api.query.system.account(address);
|
||||
const { data } = accountInfo.toJSON() as { data: { free: string } };
|
||||
const freeBalance = BigInt(data.free || 0);
|
||||
|
||||
setBalances([{
|
||||
symbol: CHAIN_CONFIG.symbol,
|
||||
name: 'Hezar Token',
|
||||
balance: formatBalance(freeBalance.toString()),
|
||||
isNative: true,
|
||||
}]);
|
||||
|
||||
const staking = await getStakingInfo(api, address);
|
||||
await refreshBalances();
|
||||
const [userScores, staking] = await Promise.all([
|
||||
getAllScores(api, address),
|
||||
getStakingInfo(api, address),
|
||||
]);
|
||||
setScores(userScores);
|
||||
setStakingInfo(staking);
|
||||
} catch (err) {
|
||||
showAlert('Failed to refresh');
|
||||
showAlert('Yenileme başarısız');
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
@@ -133,14 +73,13 @@ export function Wallet() {
|
||||
|
||||
const handleCopyAddress = async () => {
|
||||
if (!address) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(address);
|
||||
setCopied(true);
|
||||
hapticNotification('success');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
showAlert('Failed to copy address');
|
||||
showAlert('Adres kopyalanamadı');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,273 +88,307 @@ export function Wallet() {
|
||||
connectWallet();
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
hapticImpact('medium');
|
||||
disconnectWallet();
|
||||
};
|
||||
|
||||
const handleOpenExplorer = () => {
|
||||
if (!address) return;
|
||||
hapticImpact('light');
|
||||
openLink(`https://explorer.pezkuwichain.io/account/${address}`);
|
||||
};
|
||||
|
||||
const getTransactionIcon = (type: Transaction['type']) => {
|
||||
switch (type) {
|
||||
case 'send':
|
||||
return <Send className="w-4 h-4 text-red-400" />;
|
||||
case 'receive':
|
||||
return <ArrowDownToLine className="w-4 h-4 text-green-400" />;
|
||||
case 'stake':
|
||||
return <TrendingUp className="w-4 h-4 text-blue-400" />;
|
||||
case 'unstake':
|
||||
return <Clock className="w-4 h-4 text-orange-400" />;
|
||||
case 'claim':
|
||||
return <ArrowDownToLine className="w-4 h-4 text-green-400" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTimestamp = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (hours < 1) return 'Just now';
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (days < 7) return `${days}d ago`;
|
||||
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
|
||||
};
|
||||
|
||||
if (!isConnected) {
|
||||
// Not connected state
|
||||
if (!isConnected || !selectedAccount) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center gap-2 p-4 border-b border-gray-800">
|
||||
<WalletIcon className="w-5 h-5 text-green-500" />
|
||||
<h2 className="text-lg font-semibold text-white">Wallet</h2>
|
||||
</div>
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-6">
|
||||
<div className="w-20 h-20 rounded-full bg-gray-800 flex items-center justify-center mb-4">
|
||||
<WalletIcon className="w-10 h-10 text-gray-600" />
|
||||
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-cyan-500/20 to-blue-500/20 flex items-center justify-center mb-6">
|
||||
<Wallet className="w-12 h-12 text-cyan-500" />
|
||||
</div>
|
||||
<h3 className="text-white font-medium mb-2">Connect Your Wallet</h3>
|
||||
<p className="text-gray-400 text-sm text-center mb-6">
|
||||
Connect your Pezkuwi wallet to view balances, stake tokens, and manage your assets.
|
||||
<h2 className="text-white font-semibold text-xl mb-2">Cüzdanınızı Bağlayın</h2>
|
||||
<p className="text-gray-400 text-sm text-center mb-8 max-w-xs">
|
||||
Bakiyelerinizi görüntülemek, stake etmek ve işlem yapmak için Pezkuwi cüzdanınızı bağlayın.
|
||||
</p>
|
||||
<button
|
||||
<Button
|
||||
onClick={handleConnect}
|
||||
className="flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-medium transition-colors"
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-8 py-6 text-base"
|
||||
>
|
||||
<WalletIcon className="w-5 h-5" />
|
||||
Connect Wallet
|
||||
</button>
|
||||
<Wallet className="w-5 h-5 mr-2" />
|
||||
Cüzdan Bağla
|
||||
</Button>
|
||||
|
||||
<div className="mt-8 grid grid-cols-3 gap-4 w-full max-w-sm">
|
||||
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
|
||||
<Coins className="w-6 h-6 text-yellow-500 mb-2" />
|
||||
<span className="text-xs text-gray-400">HEZ & PEZ</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
|
||||
<TrendingUp className="w-6 h-6 text-purple-500 mb-2" />
|
||||
<span className="text-xs text-gray-400">Staking</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
|
||||
<Trophy className="w-6 h-6 text-cyan-500 mb-2" />
|
||||
<span className="text-xs text-gray-400">Rewards</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletIcon className="w-5 h-5 text-green-500" />
|
||||
<h2 className="text-lg font-semibold text-white">Wallet</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={cn('w-4 h-4 text-gray-400', isRefreshing && 'animate-spin')} />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDisconnect}
|
||||
className="text-xs text-gray-500 hover:text-gray-400"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<Loader2 className="w-8 h-8 text-green-500 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Address Card */}
|
||||
<div className="p-4">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-gray-400 text-sm">
|
||||
{selectedAccount?.meta?.name || 'Account'}
|
||||
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
|
||||
{/* Account Card */}
|
||||
<div className="p-4">
|
||||
<Card className="bg-gradient-to-br from-gray-900 to-gray-800 border-gray-700">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center">
|
||||
<span className="text-white font-bold">
|
||||
{selectedAccount?.meta?.name?.charAt(0) || 'P'}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleOpenExplorer}
|
||||
className="text-gray-500 hover:text-gray-400"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-white text-sm flex-1 truncate">
|
||||
{formatAddress(address || '')}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopyAddress}
|
||||
className="p-2 rounded bg-gray-700 hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">
|
||||
{selectedAccount?.meta?.name || 'Pezkuwi Hesabı'}
|
||||
</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<code className="text-gray-400 text-xs">
|
||||
{formatAddress(address || '')}
|
||||
</code>
|
||||
<button onClick={handleCopyAddress} className="p-1">
|
||||
{copied ? (
|
||||
<Check className="w-3 h-3 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<RefreshCw className={cn("w-4 h-4 text-gray-400", isRefreshing && "animate-spin")} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleOpenExplorer}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Balance Card */}
|
||||
<div className="px-4 pb-4">
|
||||
<div className="bg-gradient-to-br from-green-600 to-emerald-700 rounded-lg p-4">
|
||||
<div className="text-green-100 text-sm mb-1">Total Balance</div>
|
||||
<div className="text-3xl font-bold text-white mb-1">
|
||||
{balances[0]?.balance || '0.00'} {CHAIN_CONFIG.symbol}
|
||||
{/* Balance Display */}
|
||||
<div className="bg-black/30 rounded-lg p-4 mb-3">
|
||||
<p className="text-gray-400 text-xs mb-1">Toplam Bakiye</p>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-8 w-32 bg-gray-700" />
|
||||
) : (
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-white">
|
||||
{balances?.HEZ || '0.00'}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm">{CHAIN_CONFIG.symbol}</span>
|
||||
</div>
|
||||
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 && (
|
||||
<div className="text-green-200 text-sm">
|
||||
Staked: {stakingInfo.bonded} {CHAIN_CONFIG.symbol}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{balances?.PEZ && parseFloat(balances.PEZ) > 0 && (
|
||||
<p className="text-green-400 text-sm mt-1">
|
||||
+ {balances.PEZ} PEZ
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="px-4 pb-4">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
onClick={() => showAlert('Send feature coming soon!')}
|
||||
className="flex flex-col items-center gap-2 bg-gray-800 hover:bg-gray-700 rounded-lg p-3 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-blue-600/20 flex items-center justify-center">
|
||||
<Send className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">Send</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => showAlert('Receive feature coming soon!')}
|
||||
className="flex flex-col items-center gap-2 bg-gray-800 hover:bg-gray-700 rounded-lg p-3 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-green-600/20 flex items-center justify-center">
|
||||
<ArrowDownToLine className="w-5 h-5 text-green-400" />
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">Receive</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => showAlert('Stake feature coming soon!')}
|
||||
className="flex flex-col items-center gap-2 bg-gray-800 hover:bg-gray-700 rounded-lg p-3 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-purple-600/20 flex items-center justify-center">
|
||||
<TrendingUp className="w-5 h-5 text-purple-400" />
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">Stake</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex flex-col items-center gap-1 h-auto py-3 bg-gray-800/50 border-gray-700 hover:bg-gray-700"
|
||||
onClick={() => showAlert('Gönder özelliği yakında!')}
|
||||
>
|
||||
<Send className="w-5 h-5 text-blue-400" />
|
||||
<span className="text-xs">Gönder</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex flex-col items-center gap-1 h-auto py-3 bg-gray-800/50 border-gray-700 hover:bg-gray-700"
|
||||
onClick={() => showAlert('Al özelliği yakında!')}
|
||||
>
|
||||
<ArrowDownToLine className="w-5 h-5 text-green-400" />
|
||||
<span className="text-xs">Al</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex flex-col items-center gap-1 h-auto py-3 bg-gray-800/50 border-gray-700 hover:bg-gray-700"
|
||||
onClick={() => showAlert('Stake özelliği yakında!')}
|
||||
>
|
||||
<TrendingUp className="w-5 h-5 text-purple-400" />
|
||||
<span className="text-xs">Stake</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Staking Info */}
|
||||
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 && (
|
||||
<div className="px-4 pb-4">
|
||||
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<TrendingUp className="w-5 h-5 text-purple-500" />
|
||||
<span className="text-white font-medium">Staking Overview</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-gray-900 rounded-lg p-3">
|
||||
<div className="text-gray-400 text-xs mb-1">Bonded</div>
|
||||
<div className="text-white font-medium">{stakingInfo.bonded}</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-lg p-3">
|
||||
<div className="text-gray-400 text-xs mb-1">Active</div>
|
||||
<div className="text-white font-medium">{stakingInfo.active}</div>
|
||||
</div>
|
||||
{stakingInfo.stakingScore !== null && (
|
||||
<div className="bg-gray-900 rounded-lg p-3">
|
||||
<div className="text-gray-400 text-xs mb-1">Staking Score</div>
|
||||
<div className="text-green-500 font-medium">{stakingInfo.stakingScore}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-gray-900 rounded-lg p-3">
|
||||
<div className="text-gray-400 text-xs mb-1">Nominations</div>
|
||||
<div className="text-white font-medium">{stakingInfo.nominations.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Scores Section */}
|
||||
<div className="px-4 pb-4">
|
||||
<h3 className="text-white font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<Trophy className="w-4 h-4 text-yellow-500" />
|
||||
Puanlarınız
|
||||
</h3>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-20 bg-gray-800" />
|
||||
))}
|
||||
</div>
|
||||
) : scores ? (
|
||||
<>
|
||||
{/* Total Score Banner */}
|
||||
<Card className="bg-gradient-to-r from-purple-600 to-pink-600 border-0 mb-3">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-purple-100 text-xs">Toplam Skor</p>
|
||||
<p className="text-white text-2xl font-bold">{scores.totalScore}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Badge className="bg-white/20 text-white border-0">
|
||||
{getScoreRating(scores.totalScore)}
|
||||
</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Transactions */}
|
||||
<div className="px-4 pb-4">
|
||||
<div className="bg-gray-800 rounded-lg border border-gray-700">
|
||||
<div className="p-4 border-b border-gray-700">
|
||||
<h3 className="text-white font-medium">Recent Activity</h3>
|
||||
</div>
|
||||
{/* Individual Scores */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center">
|
||||
<Award className="w-4 h-4 text-purple-500" />
|
||||
</div>
|
||||
<span className="text-gray-400 text-xs">Trust</span>
|
||||
</div>
|
||||
<p className="text-white text-xl font-bold">{scores.trustScore}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{transactions.length === 0 ? (
|
||||
<div className="p-6 text-center text-gray-500 text-sm">
|
||||
No recent transactions
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-cyan-500/20 flex items-center justify-center">
|
||||
<Users className="w-4 h-4 text-cyan-500" />
|
||||
</div>
|
||||
<span className="text-gray-400 text-xs">Referral</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-700">
|
||||
{transactions.map(tx => (
|
||||
<div key={tx.id} className="p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center">
|
||||
{getTransactionIcon(tx.type)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white text-sm font-medium capitalize">
|
||||
{tx.type}
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs">
|
||||
{formatTimestamp(tx.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className={cn(
|
||||
'font-medium text-sm',
|
||||
tx.type === 'send' || tx.type === 'stake' ? 'text-red-400' : 'text-green-400'
|
||||
)}>
|
||||
{tx.type === 'send' || tx.type === 'stake' ? '-' : '+'}
|
||||
{tx.amount} {tx.symbol}
|
||||
</div>
|
||||
<div className={cn(
|
||||
'text-xs',
|
||||
tx.status === 'confirmed' ? 'text-gray-500' :
|
||||
tx.status === 'pending' ? 'text-yellow-500' : 'text-red-500'
|
||||
)}>
|
||||
{tx.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<p className="text-white text-xl font-bold">{scores.referralScore}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-green-500/20 flex items-center justify-center">
|
||||
<TrendingUp className="w-4 h-4 text-green-500" />
|
||||
</div>
|
||||
<span className="text-gray-400 text-xs">Staking</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-white text-xl font-bold">{scores.stakingScore}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-pink-500/20 flex items-center justify-center">
|
||||
<Star className="w-4 h-4 text-pink-500" />
|
||||
</div>
|
||||
<span className="text-gray-400 text-xs">Tiki</span>
|
||||
</div>
|
||||
<p className="text-white text-xl font-bold">{scores.tikiScore}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Staking Info */}
|
||||
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 && (
|
||||
<div className="px-4 pb-4">
|
||||
<h3 className="text-white font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-green-500" />
|
||||
Staking Durumu
|
||||
</h3>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-gray-400 text-xs mb-1">Stake Edilmiş</p>
|
||||
<p className="text-white font-bold">{stakingInfo.bonded} HEZ</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-400 text-xs mb-1">Aktif</p>
|
||||
<p className="text-green-400 font-bold">{stakingInfo.active} HEZ</p>
|
||||
</div>
|
||||
{stakingInfo.stakingScore !== null && (
|
||||
<div>
|
||||
<p className="text-gray-400 text-xs mb-1">Staking Skoru</p>
|
||||
<p className="text-purple-400 font-bold">{stakingInfo.stakingScore}/100</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-gray-400 text-xs mb-1">Nominasyonlar</p>
|
||||
<p className="text-white font-bold">{stakingInfo.nominations.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PEZ Rewards */}
|
||||
{stakingInfo.pezRewards?.hasPendingClaim && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-gray-400 text-sm">Bekleyen PEZ</span>
|
||||
</div>
|
||||
<span className="text-yellow-400 font-bold">
|
||||
{stakingInfo.pezRewards.totalClaimable} PEZ
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full mt-3 bg-yellow-600 hover:bg-yellow-700"
|
||||
onClick={() => showAlert('Claim özelliği yakında!')}
|
||||
>
|
||||
Claim Yap
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Disconnect Button */}
|
||||
<div className="px-4 pb-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-red-500/30 text-red-400 hover:bg-red-500/10"
|
||||
onClick={() => {
|
||||
hapticImpact('medium');
|
||||
disconnectWallet();
|
||||
}}
|
||||
>
|
||||
Bağlantıyı Kes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Wallet;
|
||||
export default WalletSection;
|
||||
|
||||
Reference in New Issue
Block a user