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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 23:24:41 +03:00
parent 568fca33f2
commit 2f05ec7294
10 changed files with 1657 additions and 1590 deletions
+407 -70
View File
@@ -1,91 +1,405 @@
import { useState } from 'react';
import { MessageCircle, Plus, RefreshCw, Search } from 'lucide-react';
import { ThreadCard, ForumThread } from './ThreadCard';
import { ThreadView, ForumReply } from './ThreadView';
import { useTelegram } from '../../hooks/useTelegram';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useWallet } from '@/contexts/WalletContext';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Skeleton } from '@/components/ui/skeleton';
import {
MessageCircle, Plus, RefreshCw, Search, Pin, Clock, User,
Eye, ChevronRight, ArrowLeft, Send, ThumbsUp, MessageSquare, Hash
} from 'lucide-react';
import { cn } from '@/lib/utils';
// Mock data - will be replaced with API calls
export interface ForumThread {
id: string;
title: string;
content: string;
author: string;
authorAddress?: string;
createdAt: Date;
replyCount: number;
viewCount: number;
lastReplyAt?: Date;
lastReplyAuthor?: string;
isPinned?: boolean;
tags?: string[];
}
export interface ForumReply {
id: string;
content: string;
author: string;
authorAddress?: string;
createdAt: Date;
likes: number;
userLiked?: boolean;
}
// Mock data
const mockThreads: ForumThread[] = [
{
id: '1',
title: 'Welcome to Pezkuwi Forum!',
content: 'This is the official community forum for Pezkuwi citizens. Feel free to discuss anything related to our digital state, governance, development, and more.\n\nPlease be respectful and follow our community guidelines.',
title: 'Pezkuwi Forum\'a Hoş Geldiniz!',
content: 'Bu, Pezkuwi vatandaşlarıin resmi topluluk forumudur. Dijital devletimiz, yönetişim, geliştirme ve daha fazlası hakkında serbestçe tartışabilirsiniz.\n\nLütfen saygılı olun ve topluluk kurallarımıza uyun.',
author: 'Admin',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
replyCount: 45,
viewCount: 1234,
isPinned: true,
tags: ['announcement', 'rules'],
tags: ['duyuru', 'kurallar'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 30),
lastReplyAuthor: 'NewCitizen',
lastReplyAuthor: 'YeniVatandaş',
},
{
id: '2',
title: 'How to stake HEZ and earn rewards?',
content: 'Hi everyone! I just got my first HEZ tokens and want to start staking. Can someone explain the process step by step? What is the minimum amount required?',
author: 'CryptoNewbie',
title: 'HEZ nasıl stake edilir ve ödül kazanılır?',
content: 'Herkese merhaba! İlk HEZ tokenlarımı aldım ve stake etmeye başlamak istiyorum. Biri adım adım süreci açıklayabilir mi? Minimum miktar ne kadar?',
author: 'KriptoYeni',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 5),
replyCount: 12,
viewCount: 256,
tags: ['staking', 'help'],
tags: ['staking', 'yardım'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
lastReplyAuthor: 'StakingPro',
},
{
id: '3',
title: 'Proposal: Add support for Kurdish language in the app',
content: 'As a Kurdish digital state, I think it would be great to have full Kurdish language support (Kurmanci and Sorani) in all our applications.\n\nWhat do you think?',
author: 'KurdishDev',
title: 'Öneri: Uygulamaya Kürtçe dil desteği eklenmeli',
content: 'Kürt dijital devleti olarak, tüm uygulamalarımızda tam Kürtçe dil desteği (Kurmancî ve Soranî) olması gerektiğini düşünüyorum.\n\nNe düşünüyorsunuz?',
author: 'KürtGeliştirici',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
replyCount: 28,
viewCount: 567,
tags: ['proposal', 'localization'],
tags: ['öneri', 'yerelleştirme'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 4),
lastReplyAuthor: 'LanguageExpert',
lastReplyAuthor: 'DilUzmanı',
},
{
id: '4',
title: 'Bug report: Wallet balance not updating',
content: 'After making a transfer, my wallet balance doesn\'t update immediately. I have to refresh the page multiple times. Is anyone else experiencing this issue?',
author: 'TechUser',
title: 'Hata: Cüzdan bakiyesi güncellenmiyor',
content: 'Transfer yaptıktan sonra cüzdan bakiyem hemen güncellenmiyor. Sayfayı birkaç kez yenilemem gerekiyor. Bu sorunu yaşayan başka var mı?',
author: 'TeknikKullanıcı',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 12),
replyCount: 8,
viewCount: 89,
tags: ['bug', 'wallet'],
tags: ['hata', 'cüzdan'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 6),
lastReplyAuthor: 'DevTeam',
lastReplyAuthor: 'GeliştiriciEkibi',
},
];
const mockReplies: ForumReply[] = [
{
id: '1',
content: 'Great to be here! Looking forward to participating in the community.',
author: 'NewCitizen',
content: 'Burada olmak harika! Topluluğa katılmak için sabırsızlanıyorum.',
author: 'YeniVatandaş',
createdAt: new Date(Date.now() - 1000 * 60 * 30),
likes: 5,
},
{
id: '2',
content: 'Welcome! Make sure to check out the staking guide in the docs section.',
author: 'Helper',
content: 'Hoş geldiniz! Dokümantasyon bölümündeki staking rehberini kontrol etmeyi unutmayın.',
author: 'Yardımcı',
createdAt: new Date(Date.now() - 1000 * 60 * 60),
likes: 12,
},
];
export function Forum() {
function ThreadCard({ thread, onClick }: { thread: ForumThread; onClick: () => void }) {
const { hapticSelection } = useTelegram();
const handleClick = () => {
hapticSelection();
onClick();
};
const formatDate = (date: Date) => {
const now = new Date();
const diff = now.getTime() - date.getTime();
const hours = Math.floor(diff / (1000 * 60 * 60));
const days = Math.floor(hours / 24);
if (hours < 1) return 'Az önce';
if (hours < 24) return `${hours}s önce`;
if (days < 7) return `${days}g önce`;
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
};
return (
<Card
className={cn(
"bg-gray-900 border-gray-800 cursor-pointer hover:border-gray-700 transition-all",
thread.isPinned && "border-l-4 border-l-blue-500"
)}
onClick={handleClick}
>
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
{/* Title with pin badge */}
<div className="flex items-center gap-2 mb-2">
{thread.isPinned && (
<Badge className="bg-blue-500/20 text-blue-500 border-blue-500/30 text-xs">
<Pin className="w-3 h-3 mr-1" />
Sabit
</Badge>
)}
<h3 className="font-medium text-white truncate">{thread.title}</h3>
</div>
{/* Preview */}
<p className="text-gray-400 text-sm mb-3 line-clamp-2">
{thread.content.length > 100 ? thread.content.slice(0, 100) + '...' : thread.content}
</p>
{/* Tags */}
{thread.tags && thread.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-3">
{thread.tags.slice(0, 3).map(tag => (
<div
key={tag}
className="flex items-center gap-1 text-xs bg-gray-800 text-gray-300 px-2 py-1 rounded-md"
>
<Hash className="w-3 h-3" />
{tag}
</div>
))}
</div>
)}
{/* Meta info */}
<div className="flex items-center gap-4 text-xs text-gray-500">
<div className="flex items-center gap-1">
<User className="w-3 h-3" />
<span>{thread.author}</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>{formatDate(thread.createdAt)}</span>
</div>
<div className="flex items-center gap-1">
<MessageCircle className="w-3 h-3" />
<span>{thread.replyCount}</span>
</div>
<div className="flex items-center gap-1">
<Eye className="w-3 h-3" />
<span>{thread.viewCount}</span>
</div>
</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-500 flex-shrink-0" />
</div>
{/* Last reply info */}
{thread.lastReplyAt && thread.lastReplyAuthor && (
<div className="mt-3 pt-3 border-t border-gray-800 text-xs text-gray-500">
Son yanıt: <span className="text-gray-400">{thread.lastReplyAuthor}</span>{' '}
· {formatDate(thread.lastReplyAt)}
</div>
)}
</CardContent>
</Card>
);
}
function ThreadView({
thread,
replies,
onBack,
onReply,
onLikeReply,
isConnected
}: {
thread: ForumThread;
replies: ForumReply[];
onBack: () => void;
onReply: (content: string) => void;
onLikeReply: (replyId: string) => void;
isConnected: boolean;
}) {
const { hapticImpact } = useTelegram();
const [replyContent, setReplyContent] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const formatDate = (date: Date) => {
return date.toLocaleDateString('tr-TR', {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const handleSubmitReply = async () => {
if (!replyContent.trim() || isSubmitting) return;
setIsSubmitting(true);
hapticImpact('medium');
try {
await onReply(replyContent);
setReplyContent('');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="flex flex-col h-full bg-gray-950">
{/* Header */}
<div className="flex items-center gap-3 p-4 border-b border-gray-800">
<Button
variant="ghost"
size="icon"
onClick={onBack}
className="h-8 w-8"
>
<ArrowLeft className="w-4 h-4 text-gray-400" />
</Button>
<h2 className="text-white font-semibold truncate flex-1">{thread.title}</h2>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto">
{/* Original post */}
<div className="p-4 border-b border-gray-800">
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
<User className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-white font-medium text-sm">{thread.author}</p>
<div className="flex items-center gap-1 text-xs text-gray-400">
<Clock className="w-3 h-3" />
<span>{formatDate(thread.createdAt)}</span>
</div>
</div>
</div>
{/* Tags */}
{thread.tags && thread.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-3">
{thread.tags.map(tag => (
<Badge key={tag} variant="outline" className="text-gray-400 border-gray-700 text-xs">
<Hash className="w-3 h-3 mr-1" />
{tag}
</Badge>
))}
</div>
)}
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-wrap">
{thread.content}
</p>
</CardContent>
</Card>
</div>
{/* Replies */}
<div className="p-4">
<div className="flex items-center gap-2 mb-4">
<MessageSquare className="w-4 h-4 text-blue-500" />
<span className="text-white font-medium text-sm">
{replies.length} Yanıt
</span>
</div>
<div className="space-y-3">
{replies.map(reply => (
<Card key={reply.id} className="bg-gray-900 border-gray-800">
<CardContent className="p-3">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center">
<User className="w-4 h-4 text-gray-400" />
</div>
<div>
<span className="text-sm font-medium text-white">{reply.author}</span>
<span className="text-xs text-gray-500 ml-2">
{formatDate(reply.createdAt)}
</span>
</div>
</div>
<button
onClick={() => onLikeReply(reply.id)}
className={cn(
"flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-all",
reply.userLiked
? "bg-green-500/20 text-green-500"
: "bg-gray-800 text-gray-500 hover:text-green-500"
)}
>
<ThumbsUp className={cn("w-3 h-3", reply.userLiked && "fill-current")} />
<span>{reply.likes}</span>
</button>
</div>
<p className="text-gray-300 text-sm whitespace-pre-wrap">{reply.content}</p>
</CardContent>
</Card>
))}
{replies.length === 0 && (
<div className="text-center text-gray-500 py-8">
<MessageCircle className="w-10 h-10 mx-auto mb-2 opacity-50" />
<p>Henüz yanıt yok. İlk yanıtı siz yazın!</p>
</div>
)}
</div>
</div>
</div>
{/* Reply input */}
{isConnected ? (
<div className="p-4 border-t border-gray-800 bg-gray-900">
<div className="flex gap-2">
<Input
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
placeholder="Yanıtınızı yazın..."
className="flex-1 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmitReply();
}
}}
/>
<Button
onClick={handleSubmitReply}
disabled={!replyContent.trim() || isSubmitting}
className="bg-blue-600 hover:bg-blue-700"
>
<Send className="w-4 h-4" />
</Button>
</div>
</div>
) : (
<div className="p-4 border-t border-gray-800 bg-gray-900 text-center text-gray-500 text-sm">
Yanıt yazmak için cüzdanınızı bağlayın
</div>
)}
</div>
);
}
export function ForumSection() {
const { hapticNotification, showAlert } = useTelegram();
const { selectedAccount } = usePezkuwi();
const { isConnected } = useWallet();
const [threads, setThreads] = useState<ForumThread[]>(mockThreads);
const [selectedThread, setSelectedThread] = useState<ForumThread | null>(null);
const [replies, setReplies] = useState<ForumReply[]>(mockReplies);
const [isLoading, setIsLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const isConnected = !!selectedAccount;
const handleRefresh = async () => {
setIsLoading(true);
hapticNotification('success');
@@ -96,11 +410,10 @@ export function Forum() {
const handleCreateThread = () => {
if (!isConnected) {
showAlert('Please connect your wallet to create a thread');
showAlert('Konu oluşturmak için cüzdanınızı bağlayın');
return;
}
// TODO: Implement thread creation modal
showAlert('Thread creation coming soon!');
showAlert('Konu oluşturma özelliği yakında!');
};
const handleReply = async (content: string) => {
@@ -109,7 +422,7 @@ export function Forum() {
const newReply: ForumReply = {
id: String(Date.now()),
content,
author: selectedAccount?.meta?.name || 'Anonymous',
author: selectedAccount?.meta?.name || 'Anonim',
createdAt: new Date(),
likes: 0,
};
@@ -155,57 +468,81 @@ export function Forum() {
}
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800">
<div className="flex items-center gap-2">
<MessageCircle className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-white">Forum</h2>
<div className="p-4 pb-0">
<div className="flex items-center justify-between mb-4">
<h2 className="text-white font-semibold text-lg flex items-center gap-2">
<MessageCircle className="w-5 h-5 text-blue-500" />
Forum
</h2>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={handleRefresh}
disabled={isLoading}
className="h-8 w-8"
>
<RefreshCw className={cn("w-4 h-4 text-gray-400", isLoading && "animate-spin")} />
</Button>
<Button
size="icon"
onClick={handleCreateThread}
className="h-8 w-8 bg-blue-600 hover:bg-blue-700"
>
<Plus className="w-4 h-4" />
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleRefresh}
disabled={isLoading}
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 text-gray-400 ${isLoading ? 'animate-spin' : ''}`} />
</button>
<button
onClick={handleCreateThread}
className="p-2 rounded-lg bg-green-600 hover:bg-green-700 transition-colors"
>
<Plus className="w-4 h-4 text-white" />
</button>
</div>
</div>
{/* Search */}
<div className="p-4 border-b border-gray-800">
<div className="relative">
{/* Search */}
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
<Input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search threads..."
className="w-full bg-gray-800 border border-gray-700 rounded-lg pl-9 pr-3 py-2 text-white text-sm placeholder-gray-500 focus:outline-none focus:border-green-500"
placeholder="Konularda ara..."
className="pl-9 bg-gray-900 border-gray-800 text-white placeholder:text-gray-500"
/>
</div>
{/* Stats */}
<div className="flex items-center gap-4 mb-4">
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
<MessageSquare className="w-4 h-4 text-blue-500" />
<span className="text-gray-300 text-sm">{threads.length} Konu</span>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
<Pin className="w-4 h-4 text-blue-500" />
<span className="text-gray-300 text-sm">
{threads.filter(t => t.isPinned).length} Sabitlenmiş
</span>
</div>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{sortedThreads.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500">
{/* Thread List */}
<div className="flex-1 p-4 pt-0 space-y-3">
{isLoading ? (
<>
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-32 bg-gray-800" />
))}
</>
) : sortedThreads.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
<MessageCircle className="w-12 h-12 mb-3 opacity-50" />
<p>{searchQuery ? 'No threads found' : 'No threads yet'}</p>
<p>{searchQuery ? 'Konu bulunamadı' : 'Henüz konu yok'}</p>
{!searchQuery && (
<button
<Button
variant="link"
onClick={handleCreateThread}
className="mt-4 text-green-500 hover:text-green-400"
className="mt-2 text-blue-500"
>
Create the first thread
</button>
İlk konuyu oluştur
</Button>
)}
</div>
) : (
@@ -222,4 +559,4 @@ export function Forum() {
);
}
export default Forum;
export default ForumSection;