mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-06-22 06:41:03 +00:00
Initial commit - PezkuwiChain Telegram MiniApp
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
Megaphone,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
RefreshCw,
|
||||
ExternalLink,
|
||||
Calendar,
|
||||
Eye,
|
||||
} from 'lucide-react';
|
||||
import { cn, formatDate, formatNumber } from '@/lib/utils';
|
||||
import { useTelegram } from '@/hooks/useTelegram';
|
||||
import { useAnnouncements, useAnnouncementReaction } from '@/hooks/useSupabase';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
|
||||
export function AnnouncementsSection() {
|
||||
const { hapticImpact, hapticNotification, openLink } = useTelegram();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data: announcements, isLoading, refetch, isRefetching } = useAnnouncements();
|
||||
const reactionMutation = useAnnouncementReaction();
|
||||
|
||||
const handleReaction = (id: string, reaction: 'like' | 'dislike') => {
|
||||
if (!isAuthenticated) {
|
||||
hapticNotification('error');
|
||||
// Show alert or toast here if UI library allows, for now using browser alert for clarity in dev
|
||||
// In production better to use a Toast component
|
||||
if (window.Telegram?.WebApp) {
|
||||
window.Telegram.WebApp.showAlert('Ji bo dengdanê divê tu têketî bî');
|
||||
} else {
|
||||
window.alert('Ji bo dengdanê divê tu têketî bî');
|
||||
}
|
||||
return;
|
||||
}
|
||||
hapticImpact('light');
|
||||
reactionMutation.mutate(
|
||||
{ announcementId: id, reaction },
|
||||
{ onSuccess: () => hapticNotification('success') }
|
||||
);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
hapticImpact('medium');
|
||||
refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<header className="flex-shrink-0 px-4 py-3 border-b border-border bg-background/80 backdrop-blur-sm safe-area-top">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center">
|
||||
<Megaphone className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-lg font-semibold">Ragihandin</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefetching}
|
||||
className="p-2 rounded-lg hover:bg-secondary transition-colors"
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn('w-5 h-5 text-muted-foreground', isRefetching && 'animate-spin')}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto hide-scrollbar">
|
||||
{isLoading ? (
|
||||
<div className="p-4 space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="bg-secondary/50 rounded-xl p-4 animate-pulse">
|
||||
<div className="h-4 bg-secondary rounded w-3/4 mb-3" />
|
||||
<div className="h-3 bg-secondary rounded w-full mb-2" />
|
||||
<div className="h-3 bg-secondary rounded w-2/3" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 space-y-4">
|
||||
{announcements?.map((announcement) => (
|
||||
<article
|
||||
key={announcement.id}
|
||||
className="bg-secondary/30 rounded-xl overflow-hidden border border-border/50"
|
||||
>
|
||||
{/* Image */}
|
||||
{announcement.image_url && (
|
||||
<div className="overflow-hidden bg-secondary/50">
|
||||
<img
|
||||
src={announcement.image_url}
|
||||
alt=""
|
||||
className="w-full h-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4">
|
||||
{/* Title */}
|
||||
<h2 className="font-semibold text-foreground mb-2 leading-tight">
|
||||
{announcement.title}
|
||||
</h2>
|
||||
|
||||
{/* Content */}
|
||||
<p className="text-sm text-muted-foreground mb-3 leading-relaxed">
|
||||
{announcement.content}
|
||||
</p>
|
||||
|
||||
{/* Link */}
|
||||
{announcement.link_url && (
|
||||
<button
|
||||
onClick={() => openLink(announcement.link_url as string)}
|
||||
className="flex items-center gap-1.5 text-sm text-primary mb-3 hover:underline"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
Zêdetir bixwîne
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Meta */}
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground mb-3">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
{formatDate(announcement.created_at)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
{formatNumber(announcement.views)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 pt-3 border-t border-border/50">
|
||||
<button
|
||||
onClick={() => handleReaction(announcement.id, 'like')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors',
|
||||
announcement.user_reaction === 'like'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-secondary hover:bg-secondary/80 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<ThumbsUp className="w-4 h-4" />
|
||||
{formatNumber(announcement.likes)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleReaction(announcement.id, 'dislike')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors',
|
||||
announcement.user_reaction === 'dislike'
|
||||
? 'bg-red-500/20 text-red-400'
|
||||
: 'bg-secondary hover:bg-secondary/80 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<ThumbsDown className="w-4 h-4" />
|
||||
{formatNumber(announcement.dislikes)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,862 @@
|
||||
/**
|
||||
* Forum Section - Community Discussions
|
||||
* Uses shared Supabase tables from pwap/web (forum_discussions, forum_categories)
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
MessageCircle,
|
||||
ArrowLeft,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
MessageSquare,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
Flame,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Pin,
|
||||
Lock,
|
||||
Eye,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
CheckCircle,
|
||||
Megaphone,
|
||||
Plus,
|
||||
Send,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTelegram } from '@/hooks/useTelegram';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useForum, type ForumDiscussion, type ForumReply } from '@/hooks/useForum';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
type SortBy = 'recent' | 'popular' | 'replies' | 'views';
|
||||
|
||||
export function ForumSection() {
|
||||
const { hapticImpact, hapticNotification, showAlert } = useTelegram();
|
||||
const { user: authUser } = useAuth();
|
||||
// Use authenticated user ID from backend, not initDataUnsafe
|
||||
const userId = authUser?.telegram_id?.toString() || '';
|
||||
const userName = authUser?.first_name || 'Telegram User';
|
||||
|
||||
const {
|
||||
announcements,
|
||||
categories,
|
||||
discussions,
|
||||
loading,
|
||||
refreshData,
|
||||
fetchReplies,
|
||||
createDiscussion,
|
||||
createReply,
|
||||
voteOnDiscussion,
|
||||
voteOnReply,
|
||||
incrementViewCount,
|
||||
} = useForum(userId);
|
||||
|
||||
const [view, setView] = useState<'list' | 'thread' | 'create'>('list');
|
||||
const [selectedDiscussion, setSelectedDiscussion] = useState<ForumDiscussion | null>(null);
|
||||
const [sortBy, setSortBy] = useState<SortBy>('recent');
|
||||
const [filterCategory, setFilterCategory] = useState<string>('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Thread view state
|
||||
const [replies, setReplies] = useState<ForumReply[]>([]);
|
||||
const [loadingReplies, setLoadingReplies] = useState(false);
|
||||
const [replyText, setReplyText] = useState('');
|
||||
const [submittingReply, setSubmittingReply] = useState(false);
|
||||
|
||||
// Create discussion state
|
||||
const [newTitle, setNewTitle] = useState('');
|
||||
const [newContent, setNewContent] = useState('');
|
||||
const [newCategory, setNewCategory] = useState<string>('');
|
||||
const [newTags, setNewTags] = useState('');
|
||||
const [submittingDiscussion, setSubmittingDiscussion] = useState(false);
|
||||
|
||||
const handleOpenThread = async (discussion: ForumDiscussion) => {
|
||||
hapticImpact('light');
|
||||
setSelectedDiscussion(discussion);
|
||||
setView('thread');
|
||||
|
||||
// Increment view count
|
||||
await incrementViewCount(discussion.id);
|
||||
|
||||
// Load replies
|
||||
setLoadingReplies(true);
|
||||
const loadedReplies = await fetchReplies(discussion.id);
|
||||
setReplies(loadedReplies);
|
||||
setLoadingReplies(false);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
hapticImpact('light');
|
||||
setView('list');
|
||||
setSelectedDiscussion(null);
|
||||
setReplies([]);
|
||||
setReplyText('');
|
||||
};
|
||||
|
||||
const handleOpenCreate = () => {
|
||||
hapticImpact('medium');
|
||||
setView('create');
|
||||
// Set default category if available
|
||||
if (categories.length > 0 && !newCategory) {
|
||||
setNewCategory(categories[0].id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseCreate = () => {
|
||||
hapticImpact('light');
|
||||
setView('list');
|
||||
setNewTitle('');
|
||||
setNewContent('');
|
||||
setNewTags('');
|
||||
};
|
||||
|
||||
const handleVoteDiscussion = async (voteType: 'upvote' | 'downvote') => {
|
||||
if (!selectedDiscussion || !userId) {
|
||||
showAlert('Ji bo dengdanê têkeve');
|
||||
return;
|
||||
}
|
||||
|
||||
hapticImpact('light');
|
||||
try {
|
||||
await voteOnDiscussion(selectedDiscussion.id, userId, voteType);
|
||||
hapticNotification('success');
|
||||
|
||||
// Update local state
|
||||
const updatedDiscussion = discussions.find((d) => d.id === selectedDiscussion.id);
|
||||
if (updatedDiscussion) {
|
||||
setSelectedDiscussion(updatedDiscussion);
|
||||
}
|
||||
} catch {
|
||||
hapticNotification('error');
|
||||
showAlert('Çewtî di dengdanê de');
|
||||
}
|
||||
};
|
||||
|
||||
const handleVoteReply = async (replyId: string, voteType: 'upvote' | 'downvote') => {
|
||||
if (!userId) {
|
||||
showAlert('Ji bo dengdanê têkeve');
|
||||
return;
|
||||
}
|
||||
|
||||
hapticImpact('light');
|
||||
try {
|
||||
await voteOnReply(replyId, userId, voteType);
|
||||
hapticNotification('success');
|
||||
|
||||
// Refresh replies
|
||||
if (selectedDiscussion) {
|
||||
const loadedReplies = await fetchReplies(selectedDiscussion.id);
|
||||
setReplies(loadedReplies);
|
||||
}
|
||||
} catch {
|
||||
hapticNotification('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitReply = async () => {
|
||||
if (!selectedDiscussion || !replyText.trim() || !userId) {
|
||||
showAlert('Ji kerema xwe bersiva xwe binivîse');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDiscussion.is_locked) {
|
||||
showAlert('Ev mijar kilîtkirî ye');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmittingReply(true);
|
||||
hapticImpact('medium');
|
||||
|
||||
try {
|
||||
await createReply({
|
||||
discussion_id: selectedDiscussion.id,
|
||||
content: replyText.trim(),
|
||||
author_id: userId,
|
||||
author_name: userName,
|
||||
});
|
||||
|
||||
setReplyText('');
|
||||
hapticNotification('success');
|
||||
|
||||
// Refresh replies
|
||||
const loadedReplies = await fetchReplies(selectedDiscussion.id);
|
||||
setReplies(loadedReplies);
|
||||
} catch {
|
||||
hapticNotification('error');
|
||||
showAlert('Çewtî di şandina bersivê de');
|
||||
} finally {
|
||||
setSubmittingReply(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitDiscussion = async () => {
|
||||
if (!newTitle.trim() || !newContent.trim() || !newCategory || !userId) {
|
||||
showAlert('Ji kerema xwe hemû qadan tije bike');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmittingDiscussion(true);
|
||||
hapticImpact('medium');
|
||||
|
||||
try {
|
||||
const tags = newTags
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0);
|
||||
|
||||
await createDiscussion({
|
||||
title: newTitle.trim(),
|
||||
content: newContent.trim(),
|
||||
category_id: newCategory,
|
||||
author_id: userId,
|
||||
author_name: userName,
|
||||
tags,
|
||||
});
|
||||
|
||||
hapticNotification('success');
|
||||
showAlert('Mijar hat afirandin!');
|
||||
handleCloseCreate();
|
||||
} catch {
|
||||
hapticNotification('error');
|
||||
showAlert('Çewtî di afirandina mijarê de');
|
||||
} finally {
|
||||
setSubmittingDiscussion(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getAnnouncementStyle = (type: string) => {
|
||||
switch (type) {
|
||||
case 'critical':
|
||||
return { icon: AlertTriangle, bgClass: 'bg-red-500/20 border-red-500/40 text-red-300' };
|
||||
case 'warning':
|
||||
return {
|
||||
icon: AlertTriangle,
|
||||
bgClass: 'bg-yellow-500/20 border-yellow-500/40 text-yellow-300',
|
||||
};
|
||||
case 'success':
|
||||
return { icon: CheckCircle, bgClass: 'bg-green-500/20 border-green-500/40 text-green-300' };
|
||||
default:
|
||||
return { icon: Info, bgClass: 'bg-blue-500/20 border-blue-500/40 text-blue-300' };
|
||||
}
|
||||
};
|
||||
|
||||
// Filter and sort discussions
|
||||
const filteredDiscussions = discussions
|
||||
.filter((d) => {
|
||||
const matchesSearch =
|
||||
d.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
d.content.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesCategory =
|
||||
filterCategory === 'all' || d.category?.name.toLowerCase() === filterCategory.toLowerCase();
|
||||
return matchesSearch && matchesCategory;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'popular':
|
||||
return (b.upvotes || 0) - (a.upvotes || 0);
|
||||
case 'replies':
|
||||
return b.replies_count - a.replies_count;
|
||||
case 'views':
|
||||
return b.views_count - a.views_count;
|
||||
default:
|
||||
return new Date(b.last_activity_at).getTime() - new Date(a.last_activity_at).getTime();
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh selected discussion when discussions update
|
||||
const selectedDiscussionId = selectedDiscussion?.id;
|
||||
useEffect(() => {
|
||||
if (selectedDiscussionId) {
|
||||
const updated = discussions.find((d) => d.id === selectedDiscussionId);
|
||||
if (updated) {
|
||||
setSelectedDiscussion(updated);
|
||||
}
|
||||
}
|
||||
}, [discussions, selectedDiscussionId]);
|
||||
|
||||
// Create Discussion View
|
||||
if (view === 'create') {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<header className="flex-shrink-0 px-4 py-3 border-b border-border bg-background/80 backdrop-blur-sm safe-area-top">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleCloseCreate}
|
||||
className="p-2 -ml-2 rounded-lg hover:bg-secondary"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
<h1 className="text-lg font-semibold">Mijara Nû</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmitDiscussion}
|
||||
disabled={submittingDiscussion || !newTitle.trim() || !newContent.trim()}
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
submittingDiscussion || !newTitle.trim() || !newContent.trim()
|
||||
? 'bg-secondary text-muted-foreground'
|
||||
: 'bg-primary text-primary-foreground'
|
||||
)}
|
||||
>
|
||||
{submittingDiscussion ? 'Tê şandin...' : 'Biweşîne'}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto hide-scrollbar p-4 space-y-4">
|
||||
{/* Category */}
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-2 block">Kategorî</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => {
|
||||
hapticImpact('light');
|
||||
setNewCategory(cat.id);
|
||||
}}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-lg text-sm transition-colors flex items-center gap-2',
|
||||
newCategory === cat.id
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-secondary text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<span>{cat.icon}</span>
|
||||
{cat.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-2 block">Sernav</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
placeholder="Navê mijarê..."
|
||||
className="w-full px-4 py-3 bg-secondary rounded-lg text-foreground"
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-2 block">Naverok</label>
|
||||
<textarea
|
||||
value={newContent}
|
||||
onChange={(e) => setNewContent(e.target.value)}
|
||||
placeholder="Naveroka mijarê binivîse..."
|
||||
className="w-full px-4 py-3 bg-secondary rounded-lg text-foreground min-h-[200px] resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-2 block">
|
||||
Etîket (bi virgulê cuda bike)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newTags}
|
||||
onChange={(e) => setNewTags(e.target.value)}
|
||||
placeholder="blockchain, kurd, pez..."
|
||||
className="w-full px-4 py-3 bg-secondary rounded-lg text-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Thread Detail View
|
||||
if (view === 'thread' && selectedDiscussion) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<header className="flex-shrink-0 px-4 py-3 border-b border-border bg-background/80 backdrop-blur-sm safe-area-top">
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={handleBack} className="p-2 -ml-2 rounded-lg hover:bg-secondary">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<h1 className="text-lg font-semibold truncate flex-1">{selectedDiscussion.title}</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto hide-scrollbar p-4">
|
||||
{/* Badges */}
|
||||
<div className="flex items-center gap-2 mb-3 flex-wrap">
|
||||
{selectedDiscussion.is_pinned && (
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-yellow-500/20 text-yellow-400 px-2 py-1 rounded-full">
|
||||
<Pin className="w-3 h-3" />
|
||||
Pinned
|
||||
</span>
|
||||
)}
|
||||
{selectedDiscussion.is_locked && (
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-red-500/20 text-red-400 px-2 py-1 rounded-full">
|
||||
<Lock className="w-3 h-3" />
|
||||
Kilîtkirî
|
||||
</span>
|
||||
)}
|
||||
{selectedDiscussion.category && (
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-secondary text-muted-foreground px-2 py-1 rounded-full">
|
||||
{selectedDiscussion.category.icon} {selectedDiscussion.category.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-secondary/30 rounded-xl p-4 mb-4 border border-border/50">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-sm font-bold">
|
||||
{selectedDiscussion.author_name?.charAt(0) || 'A'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
{selectedDiscussion.author_name || 'Anonymous'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatDistanceToNow(new Date(selectedDiscussion.created_at), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image if exists */}
|
||||
{selectedDiscussion.image_url && (
|
||||
<div className="mb-4 rounded-lg overflow-hidden bg-secondary/50">
|
||||
<img
|
||||
src={selectedDiscussion.image_url}
|
||||
alt=""
|
||||
className="w-full h-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-foreground leading-relaxed whitespace-pre-wrap">
|
||||
{selectedDiscussion.content}
|
||||
</p>
|
||||
|
||||
{/* Tags */}
|
||||
{selectedDiscussion.tags && selectedDiscussion.tags.length > 0 && (
|
||||
<div className="flex gap-2 mt-4 flex-wrap">
|
||||
{selectedDiscussion.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs bg-cyan-500/20 text-cyan-400 px-2 py-1 rounded-full"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats & Vote Buttons */}
|
||||
<div className="flex items-center gap-2 mt-4 pt-4 border-t border-border/50">
|
||||
<button
|
||||
onClick={() => handleVoteDiscussion('upvote')}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm transition-colors',
|
||||
selectedDiscussion.userVote === 'upvote'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-secondary text-muted-foreground hover:bg-secondary/80'
|
||||
)}
|
||||
>
|
||||
<ThumbsUp className="w-4 h-4" />
|
||||
{selectedDiscussion.upvotes || 0}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleVoteDiscussion('downvote')}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm transition-colors',
|
||||
selectedDiscussion.userVote === 'downvote'
|
||||
? 'bg-red-500/20 text-red-400'
|
||||
: 'bg-secondary text-muted-foreground hover:bg-secondary/80'
|
||||
)}
|
||||
>
|
||||
<ThumbsDown className="w-4 h-4" />
|
||||
{selectedDiscussion.downvotes || 0}
|
||||
</button>
|
||||
<span className="flex items-center gap-1 text-sm text-muted-foreground ml-auto">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
{selectedDiscussion.replies_count}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<Eye className="w-4 h-4" />
|
||||
{selectedDiscussion.views_count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Replies Section */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground mb-3 flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
Bersiv ({replies.length})
|
||||
</h3>
|
||||
|
||||
{loadingReplies ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="bg-secondary/30 rounded-xl p-4 animate-pulse">
|
||||
<div className="h-4 bg-secondary rounded w-1/4 mb-2" />
|
||||
<div className="h-3 bg-secondary rounded w-3/4" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : replies.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<MessageSquare className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">Hêj bersiv tune ye</p>
|
||||
<p className="text-xs">Yekemîn bersivê tu bide!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{replies.map((reply) => (
|
||||
<div
|
||||
key={reply.id}
|
||||
className="bg-secondary/20 rounded-xl p-3 border border-border/30"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center text-xs font-bold">
|
||||
{reply.author_name?.charAt(0) || 'A'}
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{reply.author_name || 'Anonymous'}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDistanceToNow(new Date(reply.created_at), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-foreground whitespace-pre-wrap mb-2">
|
||||
{reply.content}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleVoteReply(reply.id, 'upvote')}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors',
|
||||
reply.userVote === 'upvote'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'text-muted-foreground hover:bg-secondary'
|
||||
)}
|
||||
>
|
||||
<ThumbsUp className="w-3 h-3" />
|
||||
{reply.upvotes || 0}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleVoteReply(reply.id, 'downvote')}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors',
|
||||
reply.userVote === 'downvote'
|
||||
? 'bg-red-500/20 text-red-400'
|
||||
: 'text-muted-foreground hover:bg-secondary'
|
||||
)}
|
||||
>
|
||||
<ThumbsDown className="w-3 h-3" />
|
||||
{reply.downvotes || 0}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reply Input */}
|
||||
{!selectedDiscussion.is_locked && (
|
||||
<div className="flex-shrink-0 p-4 border-t border-border bg-background safe-area-bottom">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={replyText}
|
||||
onChange={(e) => setReplyText(e.target.value)}
|
||||
placeholder="Bersiva xwe binivîse..."
|
||||
className="flex-1 px-4 py-2.5 bg-secondary rounded-lg text-sm"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmitReply();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSubmitReply}
|
||||
disabled={submittingReply || !replyText.trim()}
|
||||
className={cn(
|
||||
'p-2.5 rounded-lg transition-colors',
|
||||
submittingReply || !replyText.trim()
|
||||
? 'bg-secondary text-muted-foreground'
|
||||
: 'bg-primary text-primary-foreground'
|
||||
)}
|
||||
>
|
||||
<Send className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Thread List View
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<header className="flex-shrink-0 px-4 py-3 border-b border-border bg-background/80 backdrop-blur-sm safe-area-top">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-500/20 flex items-center justify-center">
|
||||
<MessageCircle className="w-4 h-4 text-blue-400" />
|
||||
</div>
|
||||
<h1 className="text-lg font-semibold">Forum</h1>
|
||||
<span className="text-xs text-muted-foreground">({discussions.length})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={handleOpenCreate}
|
||||
className="p-2 rounded-lg bg-primary text-primary-foreground"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
hapticImpact('light');
|
||||
refreshData();
|
||||
}}
|
||||
disabled={loading}
|
||||
className="p-2 rounded-lg hover:bg-secondary"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-5 h-5 text-muted-foreground ${loading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative mb-3">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Mijar bigere..."
|
||||
className="w-full pl-9 pr-4 py-2 bg-secondary rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sort Tabs */}
|
||||
<div className="flex gap-1 bg-secondary/50 rounded-lg p-1">
|
||||
{[
|
||||
{ id: 'recent' as SortBy, icon: Clock, label: 'Nû' },
|
||||
{ id: 'popular' as SortBy, icon: TrendingUp, label: 'Populer' },
|
||||
{ id: 'replies' as SortBy, icon: MessageSquare, label: 'Bersiv' },
|
||||
{ id: 'views' as SortBy, icon: Eye, label: 'Dîtin' },
|
||||
].map(({ id, icon: Icon, label }) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => {
|
||||
hapticImpact('light');
|
||||
setSortBy(id);
|
||||
}}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-1 py-2 rounded-md text-xs transition-colors',
|
||||
sortBy === id ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto hide-scrollbar">
|
||||
{/* Announcements */}
|
||||
{announcements.length > 0 && (
|
||||
<div className="p-4 space-y-2">
|
||||
{announcements.map((announcement) => {
|
||||
const style = getAnnouncementStyle(announcement.type);
|
||||
const Icon = style.icon;
|
||||
return (
|
||||
<div key={announcement.id} className={`rounded-xl p-3 border ${style.bgClass}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<Megaphone className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-sm">{announcement.title}</h4>
|
||||
<p className="text-xs mt-1 opacity-90 line-clamp-2">{announcement.content}</p>
|
||||
</div>
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Categories */}
|
||||
{categories.length > 0 && (
|
||||
<div className="px-4 pb-3">
|
||||
<div className="flex gap-2 overflow-x-auto hide-scrollbar py-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
hapticImpact('light');
|
||||
setFilterCategory('all');
|
||||
}}
|
||||
className={cn(
|
||||
'flex-shrink-0 px-3 py-1.5 rounded-full text-xs transition-colors',
|
||||
filterCategory === 'all'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-secondary text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
Hemû
|
||||
</button>
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => {
|
||||
hapticImpact('light');
|
||||
setFilterCategory(category.name.toLowerCase());
|
||||
}}
|
||||
className={cn(
|
||||
'flex-shrink-0 px-3 py-1.5 rounded-full text-xs transition-colors flex items-center gap-1',
|
||||
filterCategory === category.name.toLowerCase()
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-secondary text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<span>{category.icon}</span>
|
||||
{category.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Discussions List */}
|
||||
{loading ? (
|
||||
<div className="p-4 space-y-3">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="bg-secondary/50 rounded-xl p-4 animate-pulse">
|
||||
<div className="h-4 bg-secondary rounded w-3/4 mb-2" />
|
||||
<div className="h-3 bg-secondary rounded w-1/2" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : filteredDiscussions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 p-8 text-center">
|
||||
<MessageCircle className="w-12 h-12 text-muted-foreground/50 mb-4" />
|
||||
<p className="text-muted-foreground">Mijar nehat dîtin</p>
|
||||
<p className="text-sm text-muted-foreground/70 mb-4">Filterên xwe biguhêre</p>
|
||||
<button
|
||||
onClick={handleOpenCreate}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg text-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Mijara Nû Biafirîne
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 space-y-3">
|
||||
{filteredDiscussions.map((discussion) => (
|
||||
<button
|
||||
key={discussion.id}
|
||||
onClick={() => handleOpenThread(discussion)}
|
||||
className="w-full text-left bg-secondary/30 rounded-xl p-4 border border-border/50 hover:bg-secondary/50 transition-colors"
|
||||
>
|
||||
{/* Badges */}
|
||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||
{discussion.is_pinned && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded">
|
||||
<Pin className="w-2.5 h-2.5" />
|
||||
Pinned
|
||||
</span>
|
||||
)}
|
||||
{discussion.is_locked && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] bg-red-500/20 text-red-400 px-1.5 py-0.5 rounded">
|
||||
<Lock className="w-2.5 h-2.5" />
|
||||
</span>
|
||||
)}
|
||||
{discussion.category && (
|
||||
<span className="text-[10px] bg-secondary text-muted-foreground px-1.5 py-0.5 rounded">
|
||||
{discussion.category.icon} {discussion.category.name}
|
||||
</span>
|
||||
)}
|
||||
{(discussion.upvotes || 0) > 10 && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] bg-orange-500/20 text-orange-400 px-1.5 py-0.5 rounded">
|
||||
<Flame className="w-2.5 h-2.5" />
|
||||
Trending
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Image thumbnail if exists */}
|
||||
{discussion.image_url && (
|
||||
<div className="mb-2 rounded-lg overflow-hidden bg-secondary/50">
|
||||
<img
|
||||
src={discussion.image_url}
|
||||
alt=""
|
||||
className="w-full h-auto object-contain max-h-40"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-medium text-foreground mb-1 line-clamp-2">
|
||||
{discussion.title}
|
||||
</h3>
|
||||
|
||||
{/* Meta */}
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground flex-wrap">
|
||||
<span>{discussion.author_name || 'Anonymous'}</span>
|
||||
<span>
|
||||
{formatDistanceToNow(new Date(discussion.last_activity_at), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<ThumbsUp className="w-3 h-3" />
|
||||
{(discussion.upvotes || 0) - (discussion.downvotes || 0)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<MessageSquare className="w-3 h-3" />
|
||||
{discussion.replies_count}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Eye className="w-3 h-3" />
|
||||
{discussion.views_count}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{discussion.tags && discussion.tags.length > 0 && (
|
||||
<div className="flex gap-1 mt-2 flex-wrap">
|
||||
{discussion.tags.slice(0, 3).map((tag) => (
|
||||
<span key={tag} className="text-[10px] text-cyan-400">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
{discussion.tags.length > 3 && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
+{discussion.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* Rewards Section - Referral System
|
||||
* Uses real blockchain data from ReferralContext
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Gift,
|
||||
Users,
|
||||
Trophy,
|
||||
Copy,
|
||||
Check,
|
||||
Share2,
|
||||
RefreshCw,
|
||||
Star,
|
||||
TrendingUp,
|
||||
Award,
|
||||
Zap,
|
||||
Coins,
|
||||
} from 'lucide-react';
|
||||
import { cn, formatAddress } from '@/lib/utils';
|
||||
import { useTelegram } from '@/hooks/useTelegram';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useReferral } from '@/contexts/ReferralContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { SocialLinks } from '@/components/SocialLinks';
|
||||
|
||||
// Activity tracking constants
|
||||
const ACTIVITY_STORAGE_KEY = 'pezkuwi_last_active';
|
||||
const ACTIVITY_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
export function RewardsSection() {
|
||||
const { hapticImpact, hapticNotification, shareUrl, showAlert } = useTelegram();
|
||||
const { user: authUser } = useAuth();
|
||||
const { stats, myReferrals, loading, refreshStats } = useReferral();
|
||||
const { isConnected } = useWallet();
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'referrals'>('overview');
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [timeRemaining, setTimeRemaining] = useState<string | null>(null);
|
||||
|
||||
// Check activity status
|
||||
const checkActivityStatus = useCallback(() => {
|
||||
const lastActive = localStorage.getItem(ACTIVITY_STORAGE_KEY);
|
||||
if (lastActive) {
|
||||
const lastActiveTime = parseInt(lastActive, 10);
|
||||
const now = Date.now();
|
||||
const elapsed = now - lastActiveTime;
|
||||
|
||||
if (elapsed < ACTIVITY_DURATION_MS) {
|
||||
setIsActive(true);
|
||||
const remaining = ACTIVITY_DURATION_MS - elapsed;
|
||||
const hours = Math.floor(remaining / (60 * 60 * 1000));
|
||||
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
|
||||
setTimeRemaining(`${hours}s ${minutes}d`);
|
||||
} else {
|
||||
setIsActive(false);
|
||||
setTimeRemaining(null);
|
||||
}
|
||||
} else {
|
||||
setIsActive(false);
|
||||
setTimeRemaining(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check activity status on mount and every minute
|
||||
useEffect(() => {
|
||||
// Run check after a microtask to avoid synchronous setState in effect
|
||||
const timeoutId = setTimeout(checkActivityStatus, 0);
|
||||
const interval = setInterval(checkActivityStatus, 60000);
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [checkActivityStatus]);
|
||||
|
||||
const handleActivate = () => {
|
||||
hapticNotification('success');
|
||||
localStorage.setItem(ACTIVITY_STORAGE_KEY, Date.now().toString());
|
||||
setIsActive(true);
|
||||
setTimeRemaining('24s 0d');
|
||||
showAlert('Tu niha aktîv î! 24 saet paşê dîsa bikirtîne.');
|
||||
};
|
||||
|
||||
// Telegram referral link (for sharing) - use authenticated user ID
|
||||
const referralLink = authUser?.telegram_id
|
||||
? `https://t.me/pezkuwichain_bot?start=ref_${authUser.telegram_id}`
|
||||
: 'https://t.me/pezkuwichain_bot';
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await window.navigator.clipboard.writeText(referralLink);
|
||||
setCopied(true);
|
||||
hapticNotification('success');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
showAlert('Kopî bû');
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
hapticImpact('medium');
|
||||
shareUrl(
|
||||
referralLink,
|
||||
'Pezkuwichain - Dewleta Dîjîtal a Kurd! Bi lînka min ve tev li me bibe:'
|
||||
);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
hapticImpact('medium');
|
||||
refreshStats();
|
||||
};
|
||||
|
||||
// Calculate points per referral based on position
|
||||
const getPointsForPosition = (position: number): number => {
|
||||
if (position <= 10) return 10;
|
||||
if (position <= 50) return 5;
|
||||
if (position <= 100) return 4;
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="flex flex-col h-full items-center justify-center p-8 text-center">
|
||||
<Gift className="w-16 h-16 text-purple-400 mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Xelat - Referral System</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Ji bo dîtina referral û xelatên xwe, berî her tiştî cîzdanê xwe girêde.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<header className="flex-shrink-0 px-4 py-3 border-b border-border bg-background/80 backdrop-blur-sm safe-area-top">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center">
|
||||
<Gift className="w-4 h-4 text-purple-400" />
|
||||
</div>
|
||||
<h1 className="text-lg font-semibold">Xelat</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
className="p-2 rounded-lg hover:bg-secondary"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-5 h-5 text-muted-foreground ${loading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 bg-secondary/50 rounded-lg p-1">
|
||||
{[
|
||||
{ id: 'overview' as const, label: 'Geşbîn' },
|
||||
{ id: 'referrals' as const, label: 'Referral' },
|
||||
].map(({ id, label }) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => {
|
||||
hapticImpact('light');
|
||||
setActiveTab(id);
|
||||
}}
|
||||
className={cn(
|
||||
'flex-1 py-2 rounded-md text-sm transition-colors',
|
||||
activeTab === id
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto hide-scrollbar">
|
||||
{/* Overview Tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="p-4 space-y-4">
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="bg-secondary/50 rounded-xl p-4 animate-pulse">
|
||||
<div className="h-4 bg-secondary rounded w-1/2 mb-2" />
|
||||
<div className="h-6 bg-secondary rounded w-3/4" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Score Card */}
|
||||
<div className="bg-gradient-to-br from-purple-600 to-pink-600 rounded-2xl p-4 text-white">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<p className="text-purple-100 text-sm">Pûana Referral</p>
|
||||
<p className="text-4xl font-bold">{stats?.referralScore ?? 0}</p>
|
||||
</div>
|
||||
<div className="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<Trophy className="w-8 h-8" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-purple-100">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>Max pûan: 500</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Refer More Card */}
|
||||
<div className="bg-gradient-to-r from-amber-500/20 to-orange-500/20 border border-amber-500/30 rounded-xl p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-amber-500/30 flex items-center justify-center flex-shrink-0">
|
||||
<Coins className="w-5 h-5 text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-amber-100 mb-1">
|
||||
زیاتر ڕیفەر بکە، زیاتر قازانج بکە!
|
||||
</h4>
|
||||
<p className="text-sm text-amber-200/80">
|
||||
هەر کەسێک بهێنیت، HEZ و PEZ وەک خەڵات وەردەگریت. زیاتر ڕیفەر = زیاتر خەڵات!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Users className="w-4 h-4 text-green-400" />
|
||||
<span className="text-xs text-muted-foreground">Referral</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{stats?.referralCount ?? 0}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">KYC pejirandî</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Award className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-xs text-muted-foreground">Referrer</span>
|
||||
</div>
|
||||
{stats?.whoInvitedMe ? (
|
||||
<p className="text-sm font-mono text-foreground truncate">
|
||||
{formatAddress(stats.whoInvitedMe, 6)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Tune</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">Min vexwand</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Referral Notification */}
|
||||
{stats?.pendingReferral && (
|
||||
<div className="bg-blue-900/20 border border-blue-600/30 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-600/30 flex items-center justify-center">
|
||||
<Award className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-white font-semibold">Referral li bendê</div>
|
||||
<div className="text-sm text-blue-300">
|
||||
KYC temam bike ji bo pejirandina referral ji{' '}
|
||||
<span className="font-mono">
|
||||
{formatAddress(stats.pendingReferral, 6)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Invite Card */}
|
||||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||||
<h3 className="font-medium text-foreground mb-3 flex items-center gap-2">
|
||||
<Share2 className="w-4 h-4 text-primary" />
|
||||
Hevalên xwe vexwîne
|
||||
</h3>
|
||||
|
||||
<div className="bg-background rounded-lg p-3 mb-3">
|
||||
<p className="text-xs text-muted-foreground mb-1">Lînka te</p>
|
||||
<code className="text-sm text-foreground break-all">{referralLink}</code>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2.5 rounded-lg text-sm font-medium transition-colors',
|
||||
copied
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-secondary hover:bg-secondary/80'
|
||||
)}
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
{copied ? 'Kopî bû!' : 'Kopî bike'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="flex-1 flex items-center justify-center gap-2 py-2.5 bg-primary rounded-lg text-primary-foreground text-sm font-medium"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
Parve bike
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Score System */}
|
||||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||||
<h3 className="font-medium text-foreground mb-3 flex items-center gap-2">
|
||||
<Star className="w-4 h-4 text-yellow-400" />
|
||||
Sîstema pûanan
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between py-2 border-b border-border/30">
|
||||
<span className="text-muted-foreground">1-10 referral</span>
|
||||
<span className="text-green-400 font-medium">×10 pûan</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-border/30">
|
||||
<span className="text-muted-foreground">11-50 referral</span>
|
||||
<span className="text-green-400 font-medium">100 + ×5</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-border/30">
|
||||
<span className="text-muted-foreground">51-100 referral</span>
|
||||
<span className="text-green-400 font-medium">300 + ×4</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2">
|
||||
<span className="text-muted-foreground">101+ referral</span>
|
||||
<span className="text-yellow-400 font-medium">500 (Max)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* I am Active Button */}
|
||||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap
|
||||
className={cn('w-5 h-5', isActive ? 'text-green-400' : 'text-gray-400')}
|
||||
/>
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">Rewşa Aktîvbûnê</h3>
|
||||
{isActive && timeRemaining && (
|
||||
<p className="text-xs text-green-400">Dem: {timeRemaining} maye</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'px-2 py-1 rounded-full text-xs font-medium',
|
||||
isActive ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
||||
)}
|
||||
>
|
||||
{isActive ? 'Aktîv' : 'Ne Aktîv'}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Her 24 saet carekê bikirtîne da ku aktîv bimînî û xelatên zêdetir qezenc bikî!
|
||||
</p>
|
||||
<button
|
||||
onClick={handleActivate}
|
||||
disabled={isActive}
|
||||
className={cn(
|
||||
'w-full py-3 rounded-lg font-medium flex items-center justify-center gap-2 transition-all',
|
||||
isActive
|
||||
? 'bg-green-500/20 text-green-400 cursor-not-allowed'
|
||||
: 'bg-gradient-to-r from-green-500 to-emerald-600 text-white hover:opacity-90'
|
||||
)}
|
||||
>
|
||||
<Zap className="w-5 h-5" />
|
||||
{isActive ? 'Tu Aktîv î!' : 'Ez Aktîv im!'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Social Links */}
|
||||
<SocialLinks />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Referrals Tab */}
|
||||
{activeTab === 'referrals' && (
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Refer More Card */}
|
||||
<div className="bg-gradient-to-r from-amber-500/20 to-orange-500/20 border border-amber-500/30 rounded-xl p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-amber-500/30 flex items-center justify-center flex-shrink-0">
|
||||
<Coins className="w-5 h-5 text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-amber-100 mb-1">
|
||||
زیاتر ڕیفەر بکە، زیاتر قازانج بکە!
|
||||
</h4>
|
||||
<p className="text-sm text-amber-200/80">
|
||||
هەر کەسێک بهێنیت، HEZ و PEZ وەک خەڵات وەردەگریت. زیاتر ڕیفەر = زیاتر خەڵات!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* I am Active Button */}
|
||||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className={cn('w-5 h-5', isActive ? 'text-green-400' : 'text-gray-400')} />
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">Rewşa Aktîvbûnê</h3>
|
||||
{isActive && timeRemaining && (
|
||||
<p className="text-xs text-green-400">Dem: {timeRemaining} maye</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'px-2 py-1 rounded-full text-xs font-medium',
|
||||
isActive ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
||||
)}
|
||||
>
|
||||
{isActive ? 'Aktîv' : 'Ne Aktîv'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleActivate}
|
||||
disabled={isActive}
|
||||
className={cn(
|
||||
'w-full py-3 rounded-lg font-medium flex items-center justify-center gap-2 transition-all',
|
||||
isActive
|
||||
? 'bg-green-500/20 text-green-400 cursor-not-allowed'
|
||||
: 'bg-gradient-to-r from-green-500 to-emerald-600 text-white hover:opacity-90'
|
||||
)}
|
||||
>
|
||||
<Zap className="w-5 h-5" />
|
||||
{isActive ? 'Tu Aktîv î!' : 'Ez Aktîv im!'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div key={i} className="bg-secondary/50 rounded-xl p-4 animate-pulse">
|
||||
<div className="h-4 bg-secondary rounded w-2/3" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : myReferrals.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-muted-foreground mb-2">
|
||||
{myReferrals.length} referral (KYC pejirandî)
|
||||
</div>
|
||||
{myReferrals.map((referralAddress, index) => (
|
||||
<div
|
||||
key={referralAddress}
|
||||
className="bg-secondary/30 rounded-xl p-4 border border-border/50 flex items-center gap-3"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-green-500/20 flex items-center justify-center text-sm font-bold text-green-400">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<code className="text-sm text-foreground">
|
||||
{formatAddress(referralAddress, 8)}
|
||||
</code>
|
||||
<p className="text-xs text-green-400">KYC Pejirandî</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-green-400 text-sm font-medium">
|
||||
+{getPointsForPosition(index + 1)} pûan
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Users className="w-12 h-12 text-muted-foreground mb-4" />
|
||||
<p className="text-muted-foreground">Hêj referralên te tune ne</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">Lînka xwe parve bike!</p>
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="mt-4 flex items-center gap-2 px-4 py-2 bg-primary rounded-lg text-primary-foreground text-sm font-medium"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
Parve bike
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Wallet Section
|
||||
* Main wallet interface with create, import, connect, and dashboard flows
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Wallet, AlertTriangle, RefreshCw } from 'lucide-react';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import {
|
||||
WalletSetup,
|
||||
WalletCreate,
|
||||
WalletImport,
|
||||
WalletConnect,
|
||||
WalletDashboard,
|
||||
} from '@/components/wallet';
|
||||
import { LoadingScreen } from '@/components/LoadingScreen';
|
||||
import { VersionInfo } from '@/components/VersionInfo';
|
||||
|
||||
type Screen = 'loading' | 'auth-error' | 'setup' | 'create' | 'import' | 'connect' | 'dashboard';
|
||||
type UserScreen = 'create' | 'import' | null;
|
||||
|
||||
export function WalletSection() {
|
||||
const { isInitialized, isConnected, hasWallet, deleteWalletData } = useWallet();
|
||||
const { isAuthenticated, isLoading: authLoading, signIn } = useAuth();
|
||||
const [userScreen, setUserScreen] = useState<UserScreen>(null);
|
||||
|
||||
// Derive screen from wallet state and user navigation
|
||||
const screen = useMemo<Screen>(() => {
|
||||
// Auth loading - wait
|
||||
if (authLoading) return 'loading';
|
||||
// Wallet not initialized yet - wait
|
||||
if (!isInitialized) return 'loading';
|
||||
// Auth failed - show error
|
||||
if (!isAuthenticated) return 'auth-error';
|
||||
// Connected - show dashboard
|
||||
if (isConnected) return 'dashboard';
|
||||
// User navigating to create/import
|
||||
if (userScreen) return userScreen;
|
||||
// Has wallet but not connected
|
||||
if (hasWallet) return 'connect';
|
||||
// No wallet - show setup
|
||||
return 'setup';
|
||||
}, [authLoading, isInitialized, isAuthenticated, isConnected, hasWallet, userScreen]);
|
||||
|
||||
// Handle wallet deletion
|
||||
const handleDeleteWallet = () => {
|
||||
deleteWalletData();
|
||||
setUserScreen(null);
|
||||
};
|
||||
|
||||
// Reset user screen when wallet state changes
|
||||
const handleComplete = () => setUserScreen(null);
|
||||
|
||||
// Handle retry auth
|
||||
const handleRetryAuth = () => {
|
||||
signIn();
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (screen === 'loading') {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Header />
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<LoadingScreen />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Auth error state
|
||||
if (screen === 'auth-error') {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Header />
|
||||
<div className="flex-1 flex items-center justify-center p-4">
|
||||
<div className="text-center space-y-6">
|
||||
<div className="w-16 h-16 mx-auto bg-red-500/20 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle className="w-8 h-8 text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-2">Têketin Têk Çû</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Ji kerema xwe piştrast bikin ku hûn vê app-ê di nav Telegram de vedikin
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRetryAuth}
|
||||
className="px-6 py-3 bg-primary text-primary-foreground rounded-xl font-semibold flex items-center gap-2 mx-auto"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Dîsa Biceribîne
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Header />
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{screen === 'setup' && (
|
||||
<WalletSetup
|
||||
onCreate={() => setUserScreen('create')}
|
||||
onImport={() => setUserScreen('import')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === 'create' && (
|
||||
<WalletCreate onComplete={handleComplete} onBack={() => setUserScreen(null)} />
|
||||
)}
|
||||
|
||||
{screen === 'import' && (
|
||||
<WalletImport onComplete={handleComplete} onBack={() => setUserScreen(null)} />
|
||||
)}
|
||||
|
||||
{screen === 'connect' && (
|
||||
<WalletConnect onConnected={handleComplete} onDelete={handleDeleteWallet} />
|
||||
)}
|
||||
|
||||
{screen === 'dashboard' && <WalletDashboard onDisconnect={handleComplete} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Header component
|
||||
function Header() {
|
||||
return (
|
||||
<header className="flex-shrink-0 px-4 py-3 border-b border-border bg-background/80 backdrop-blur-sm safe-area-top">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-cyan-500/20 flex items-center justify-center">
|
||||
<Wallet className="w-4 h-4 text-cyan-400" />
|
||||
</div>
|
||||
<h1 className="text-lg font-semibold">Berîk</h1>
|
||||
</div>
|
||||
<VersionInfo />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user