mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 04:27:56 +00:00
feat(mobile): add Forum screen with categories and thread listing
- Created ForumScreen.tsx with category browsing and thread views - Support for pinned posts and locked threads - Mock data structure ready for Supabase integration - View counts, reply counts, and time-ago formatting - Pull-to-refresh functionality - FAB for creating new threads - Added Forum tab to navigation (9 tabs total)
This commit is contained in:
@@ -9,6 +9,7 @@ import WalletScreen from '../screens/WalletScreen';
|
||||
import SwapScreen from '../screens/SwapScreen';
|
||||
import P2PScreen from '../screens/P2PScreen';
|
||||
import EducationScreen from '../screens/EducationScreen';
|
||||
import ForumScreen from '../screens/ForumScreen';
|
||||
import BeCitizenScreen from '../screens/BeCitizenScreen';
|
||||
import ReferralScreen from '../screens/ReferralScreen';
|
||||
import ProfileScreen from '../screens/ProfileScreen';
|
||||
@@ -19,6 +20,7 @@ export type BottomTabParamList = {
|
||||
Swap: undefined;
|
||||
P2P: undefined;
|
||||
Education: undefined;
|
||||
Forum: undefined;
|
||||
BeCitizen: undefined;
|
||||
Referral: undefined;
|
||||
Profile: undefined;
|
||||
@@ -112,6 +114,18 @@ const BottomTabNavigator: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tab.Screen
|
||||
name="Forum"
|
||||
component={ForumScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ color, focused }) => (
|
||||
<Text style={[styles.icon, { color }]}>
|
||||
{focused ? '💬' : '📝'}
|
||||
</Text>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tab.Screen
|
||||
name="BeCitizen"
|
||||
component={BeCitizenScreen}
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, Badge } from '../components';
|
||||
import { KurdistanColors, AppColors } from '../theme/colors';
|
||||
|
||||
interface ForumThread {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
author: string;
|
||||
category: string;
|
||||
replies_count: number;
|
||||
views_count: number;
|
||||
created_at: string;
|
||||
last_activity: string;
|
||||
is_pinned: boolean;
|
||||
is_locked: boolean;
|
||||
}
|
||||
|
||||
interface ForumCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
threads_count: number;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const CATEGORIES: ForumCategory[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'General Discussion',
|
||||
description: 'General topics about PezkuwiChain',
|
||||
threads_count: 42,
|
||||
icon: '💬',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Governance',
|
||||
description: 'Discuss proposals and governance',
|
||||
threads_count: 28,
|
||||
icon: '🏛️',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Technical',
|
||||
description: 'Development and technical discussions',
|
||||
threads_count: 35,
|
||||
icon: '⚙️',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Trading',
|
||||
description: 'Market discussions and trading',
|
||||
threads_count: 18,
|
||||
icon: '📈',
|
||||
},
|
||||
];
|
||||
|
||||
// Mock data - will be replaced with Supabase
|
||||
const MOCK_THREADS: ForumThread[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Welcome to PezkuwiChain Forum!',
|
||||
content: 'Introduce yourself and join the community...',
|
||||
author: '5GrwV...xQjz',
|
||||
category: 'General Discussion',
|
||||
replies_count: 24,
|
||||
views_count: 156,
|
||||
created_at: '2024-01-15T10:00:00Z',
|
||||
last_activity: '2024-01-20T14:30:00Z',
|
||||
is_pinned: true,
|
||||
is_locked: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'New Governance Proposal: Treasury Allocation',
|
||||
content: 'Discussion about treasury fund allocation...',
|
||||
author: '5HpG8...kLm2',
|
||||
category: 'Governance',
|
||||
replies_count: 45,
|
||||
views_count: 289,
|
||||
created_at: '2024-01-18T09:15:00Z',
|
||||
last_activity: '2024-01-20T16:45:00Z',
|
||||
is_pinned: false,
|
||||
is_locked: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'How to stake PEZ tokens?',
|
||||
content: 'Guide for staking PEZ tokens...',
|
||||
author: '5FHne...pQr8',
|
||||
category: 'General Discussion',
|
||||
replies_count: 12,
|
||||
views_count: 98,
|
||||
created_at: '2024-01-19T11:20:00Z',
|
||||
last_activity: '2024-01-20T13:10:00Z',
|
||||
is_pinned: false,
|
||||
is_locked: false,
|
||||
},
|
||||
];
|
||||
|
||||
type ViewType = 'categories' | 'threads';
|
||||
|
||||
const ForumScreen: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [viewType, setViewType] = useState<ViewType>('categories');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [threads, setThreads] = useState<ForumThread[]>(MOCK_THREADS);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchThreads = async (categoryId?: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// TODO: Fetch from Supabase
|
||||
// const { data } = await supabase
|
||||
// .from('forum_threads')
|
||||
// .select('*')
|
||||
// .eq('category_id', categoryId)
|
||||
// .order('is_pinned', { ascending: false })
|
||||
// .order('last_activity', { ascending: false });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
setThreads(MOCK_THREADS);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch threads:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fetchThreads(selectedCategory || undefined);
|
||||
};
|
||||
|
||||
const handleCategoryPress = (categoryId: string, categoryName: string) => {
|
||||
setSelectedCategory(categoryId);
|
||||
setViewType('threads');
|
||||
fetchThreads(categoryId);
|
||||
};
|
||||
|
||||
const formatTimeAgo = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||
|
||||
if (seconds < 60) return 'Just now';
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
||||
return `${Math.floor(seconds / 86400)}d ago`;
|
||||
};
|
||||
|
||||
const renderCategoryCard = ({ item }: { item: ForumCategory }) => (
|
||||
<TouchableOpacity onPress={() => handleCategoryPress(item.id, item.name)}>
|
||||
<Card style={styles.categoryCard}>
|
||||
<View style={styles.categoryHeader}>
|
||||
<View style={styles.categoryIcon}>
|
||||
<Text style={styles.categoryIconText}>{item.icon}</Text>
|
||||
</View>
|
||||
<View style={styles.categoryInfo}>
|
||||
<Text style={styles.categoryName}>{item.name}</Text>
|
||||
<Text style={styles.categoryDescription} numberOfLines={2}>
|
||||
{item.description}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.categoryFooter}>
|
||||
<Text style={styles.categoryStats}>
|
||||
{item.threads_count} threads
|
||||
</Text>
|
||||
<Text style={styles.categoryArrow}>→</Text>
|
||||
</View>
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const renderThreadCard = ({ item }: { item: ForumThread }) => (
|
||||
<TouchableOpacity>
|
||||
<Card style={styles.threadCard}>
|
||||
{/* Thread Header */}
|
||||
<View style={styles.threadHeader}>
|
||||
{item.is_pinned && (
|
||||
<View style={styles.pinnedBadge}>
|
||||
<Text style={styles.pinnedIcon}>📌</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.threadTitle} numberOfLines={2}>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.is_locked && (
|
||||
<Text style={styles.lockedIcon}>🔒</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Thread Meta */}
|
||||
<View style={styles.threadMeta}>
|
||||
<Text style={styles.threadAuthor}>by {item.author}</Text>
|
||||
<Badge text={item.category} variant="outline" />
|
||||
</View>
|
||||
|
||||
{/* Thread Stats */}
|
||||
<View style={styles.threadStats}>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statIcon}>💬</Text>
|
||||
<Text style={styles.statText}>{item.replies_count}</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statIcon}>👁️</Text>
|
||||
<Text style={styles.statText}>{item.views_count}</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statIcon}>🕐</Text>
|
||||
<Text style={styles.statText}>
|
||||
{formatTimeAgo(item.last_activity)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const renderEmptyState = () => (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={styles.emptyIcon}>💬</Text>
|
||||
<Text style={styles.emptyTitle}>No Threads Yet</Text>
|
||||
<Text style={styles.emptyText}>
|
||||
Be the first to start a discussion in this category
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.title}>
|
||||
{viewType === 'categories' ? 'Forum' : 'Threads'}
|
||||
</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{viewType === 'categories'
|
||||
? 'Join the community discussion'
|
||||
: selectedCategory || 'All threads'}
|
||||
</Text>
|
||||
</View>
|
||||
{viewType === 'threads' && (
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => setViewType('categories')}
|
||||
>
|
||||
<Text style={styles.backButtonText}>← Back</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
{loading && !refreshing ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
|
||||
<Text style={styles.loadingText}>Loading...</Text>
|
||||
</View>
|
||||
) : viewType === 'categories' ? (
|
||||
<FlatList
|
||||
data={CATEGORIES}
|
||||
renderItem={renderCategoryCard}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.listContent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={KurdistanColors.kesk}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FlatList
|
||||
data={threads}
|
||||
renderItem={renderThreadCard}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.listContent}
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={KurdistanColors.kesk}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Create Thread FAB */}
|
||||
{viewType === 'threads' && (
|
||||
<TouchableOpacity style={styles.fab}>
|
||||
<Text style={styles.fabIcon}>✏️</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: AppColors.background,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
padding: 16,
|
||||
paddingBottom: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: '700',
|
||||
color: '#000',
|
||||
marginBottom: 4,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
},
|
||||
backButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
backButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: KurdistanColors.kesk,
|
||||
},
|
||||
listContent: {
|
||||
padding: 16,
|
||||
paddingTop: 8,
|
||||
},
|
||||
categoryCard: {
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
categoryHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
},
|
||||
categoryIcon: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#F0F9F4',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
categoryIconText: {
|
||||
fontSize: 24,
|
||||
},
|
||||
categoryInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
categoryName: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#000',
|
||||
marginBottom: 4,
|
||||
},
|
||||
categoryDescription: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
lineHeight: 20,
|
||||
},
|
||||
categoryFooter: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#F0F0F0',
|
||||
},
|
||||
categoryStats: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
},
|
||||
categoryArrow: {
|
||||
fontSize: 20,
|
||||
color: KurdistanColors.kesk,
|
||||
},
|
||||
threadCard: {
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
threadHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 8,
|
||||
},
|
||||
pinnedBadge: {
|
||||
marginRight: 8,
|
||||
},
|
||||
pinnedIcon: {
|
||||
fontSize: 16,
|
||||
},
|
||||
threadTitle: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#000',
|
||||
lineHeight: 22,
|
||||
},
|
||||
lockedIcon: {
|
||||
fontSize: 16,
|
||||
marginLeft: 8,
|
||||
},
|
||||
threadMeta: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
threadAuthor: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
},
|
||||
threadStats: {
|
||||
flexDirection: 'row',
|
||||
gap: 16,
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#F0F0F0',
|
||||
},
|
||||
statItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
statIcon: {
|
||||
fontSize: 14,
|
||||
},
|
||||
statText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
},
|
||||
emptyState: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 60,
|
||||
},
|
||||
emptyIcon: {
|
||||
fontSize: 64,
|
||||
marginBottom: 16,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#000',
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: KurdistanColors.kesk,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
},
|
||||
fabIcon: {
|
||||
fontSize: 24,
|
||||
},
|
||||
});
|
||||
|
||||
export default ForumScreen;
|
||||
Reference in New Issue
Block a user