From 0c06e72c4ecc6028ea78bc747b0a86607bd02e83 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 03:21:56 +0000 Subject: [PATCH 1/7] 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) --- mobile/src/navigation/BottomTabNavigator.tsx | 14 + mobile/src/screens/ForumScreen.tsx | 512 +++++++++++++++++++ 2 files changed, 526 insertions(+) create mode 100644 mobile/src/screens/ForumScreen.tsx diff --git a/mobile/src/navigation/BottomTabNavigator.tsx b/mobile/src/navigation/BottomTabNavigator.tsx index 59d25d42..77028a51 100644 --- a/mobile/src/navigation/BottomTabNavigator.tsx +++ b/mobile/src/navigation/BottomTabNavigator.tsx @@ -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 = () => { }} /> + ( + + {focused ? '๐Ÿ’ฌ' : '๐Ÿ“'} + + ), + }} + /> + { + const { t } = useTranslation(); + + const [viewType, setViewType] = useState('categories'); + const [selectedCategory, setSelectedCategory] = useState(null); + const [threads, setThreads] = useState(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 }) => ( + handleCategoryPress(item.id, item.name)}> + + + + {item.icon} + + + {item.name} + + {item.description} + + + + + + {item.threads_count} threads + + โ†’ + + + + ); + + const renderThreadCard = ({ item }: { item: ForumThread }) => ( + + + {/* Thread Header */} + + {item.is_pinned && ( + + ๐Ÿ“Œ + + )} + + {item.title} + + {item.is_locked && ( + ๐Ÿ”’ + )} + + + {/* Thread Meta */} + + by {item.author} + + + + {/* Thread Stats */} + + + ๐Ÿ’ฌ + {item.replies_count} + + + ๐Ÿ‘๏ธ + {item.views_count} + + + ๐Ÿ• + + {formatTimeAgo(item.last_activity)} + + + + + + ); + + const renderEmptyState = () => ( + + ๐Ÿ’ฌ + No Threads Yet + + Be the first to start a discussion in this category + + + ); + + return ( + + {/* Header */} + + + + {viewType === 'categories' ? 'Forum' : 'Threads'} + + + {viewType === 'categories' + ? 'Join the community discussion' + : selectedCategory || 'All threads'} + + + {viewType === 'threads' && ( + setViewType('categories')} + > + โ† Back + + )} + + + {/* Content */} + {loading && !refreshing ? ( + + + Loading... + + ) : viewType === 'categories' ? ( + item.id} + contentContainerStyle={styles.listContent} + refreshControl={ + + } + /> + ) : ( + item.id} + contentContainerStyle={styles.listContent} + ListEmptyComponent={renderEmptyState} + refreshControl={ + + } + /> + )} + + {/* Create Thread FAB */} + {viewType === 'threads' && ( + + โœ๏ธ + + )} + + ); +}; + +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; From 447dcbc1228ebe740f1eb17486c9ec607e414d64 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 03:25:00 +0000 Subject: [PATCH 2/7] feat(mobile): expand GovernanceScreen with elections and parliament status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added tab interface: Proposals, Elections, Parliament - Presidential election support with single-candidate voting - Parliamentary election support with multi-candidate voting - Constitutional Court election tracking - Candidate cards with trust scores and vote percentages - Election voting modal with candidate selection - Parliament status (0/27 members) - Dรฎwan (Constitutional Court) status (0/9 judges) - Visual progress bars for candidate votes - Mock election data ready for pallet-tiki integration - Consistent Kurdistan color palette styling --- mobile/src/screens/GovernanceScreen.tsx | 578 ++++++++++++++++++++++-- 1 file changed, 548 insertions(+), 30 deletions(-) diff --git a/mobile/src/screens/GovernanceScreen.tsx b/mobile/src/screens/GovernanceScreen.tsx index 8f515b09..52ad6baf 100644 --- a/mobile/src/screens/GovernanceScreen.tsx +++ b/mobile/src/screens/GovernanceScreen.tsx @@ -7,6 +7,8 @@ import { RefreshControl, Alert, Pressable, + TouchableOpacity, + FlatList, } from 'react-native'; import { usePolkadot } from '../contexts/PolkadotContext'; import { AppColors, KurdistanColors } from '../theme/colors'; @@ -31,6 +33,27 @@ interface Proposal { endBlock: number; } +interface Election { + id: number; + type: 'Presidential' | 'Parliamentary' | 'Constitutional Court'; + status: 'Registration' | 'Campaign' | 'Voting' | 'Completed'; + candidates: Candidate[]; + totalVotes: number; + endBlock: number; + currentBlock: number; +} + +interface Candidate { + id: string; + name: string; + votes: number; + percentage: number; + party?: string; + trustScore: number; +} + +type TabType = 'proposals' | 'elections' | 'parliament'; + /** * Governance Screen * View proposals, vote, participate in governance @@ -38,16 +61,22 @@ interface Proposal { */ export default function GovernanceScreen() { const { api, selectedAccount, isApiReady } = usePolkadot(); + const [activeTab, setActiveTab] = useState('proposals'); const [proposals, setProposals] = useState([]); + const [elections, setElections] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [selectedProposal, setSelectedProposal] = useState(null); + const [selectedElection, setSelectedElection] = useState(null); const [voteSheetVisible, setVoteSheetVisible] = useState(false); + const [electionSheetVisible, setElectionSheetVisible] = useState(false); const [voting, setVoting] = useState(false); + const [votedCandidates, setVotedCandidates] = useState([]); useEffect(() => { if (isApiReady && selectedAccount) { fetchProposals(); + fetchElections(); } }, [isApiReady, selectedAccount]); @@ -100,6 +129,43 @@ export default function GovernanceScreen() { } }; + const fetchElections = async () => { + try { + // Mock elections data + // In production, this would fetch from pallet-tiki or election pallet + const mockElections: Election[] = [ + { + id: 1, + type: 'Presidential', + status: 'Voting', + totalVotes: 45678, + endBlock: 1000000, + currentBlock: 995000, + candidates: [ + { id: '1', name: 'Candidate A', votes: 23456, percentage: 51.3, trustScore: 850 }, + { id: '2', name: 'Candidate B', votes: 22222, percentage: 48.7, trustScore: 780 } + ] + }, + { + id: 2, + type: 'Parliamentary', + status: 'Campaign', + totalVotes: 12340, + endBlock: 1200000, + currentBlock: 995000, + candidates: [ + { id: '3', name: 'Candidate C', votes: 5678, percentage: 46.0, party: 'Green Party', trustScore: 720 }, + { id: '4', name: 'Candidate D', votes: 4567, percentage: 37.0, party: 'Democratic Alliance', trustScore: 690 }, + { id: '5', name: 'Candidate E', votes: 2095, percentage: 17.0, party: 'Independent', trustScore: 650 } + ] + } + ]; + setElections(mockElections); + } catch (error) { + console.error('Error fetching elections:', error); + } + }; + const handleVote = async (approve: boolean) => { if (!selectedProposal) return; @@ -132,6 +198,46 @@ export default function GovernanceScreen() { } }; + const handleElectionVote = (candidateId: string) => { + if (!selectedElection) return; + + if (selectedElection.type === 'Parliamentary') { + // Multiple selection for Parliamentary + setVotedCandidates(prev => + prev.includes(candidateId) + ? prev.filter(id => id !== candidateId) + : [...prev, candidateId] + ); + } else { + // Single selection for Presidential + setVotedCandidates([candidateId]); + } + }; + + const submitElectionVote = async () => { + if (votedCandidates.length === 0) { + Alert.alert('Error', 'Please select at least one candidate'); + return; + } + + try { + setVoting(true); + // TODO: Submit votes to blockchain via pallet-tiki + // await api.tx.tiki.voteInElection(electionId, candidateIds).signAndSend(...) + + Alert.alert('Success', 'Your vote has been recorded!'); + setElectionSheetVisible(false); + setSelectedElection(null); + setVotedCandidates([]); + fetchElections(); + } catch (error: any) { + console.error('Election voting error:', error); + Alert.alert('Error', error.message || 'Failed to submit vote'); + } finally { + setVoting(false); + } + }; + if (loading && proposals.length === 0) { return ( @@ -144,6 +250,44 @@ export default function GovernanceScreen() { return ( + {/* Header */} + + Governance + + Participate in digital democracy + + + + {/* Tabs */} + + setActiveTab('proposals')} + > + + Proposals ({proposals.length}) + + + + setActiveTab('elections')} + > + + Elections ({elections.length}) + + + + setActiveTab('parliament')} + > + + Parliament + + + + { setRefreshing(true); fetchProposals(); + fetchElections(); }} /> } > - {/* Header */} - - Governance - - Participate in digital democracy - - - {/* Stats */} @@ -171,31 +308,97 @@ export default function GovernanceScreen() { Active Proposals - 1,234 - Total Voters + {elections.length} + Active Elections - {/* Proposals List */} - Active Proposals - {proposals.length === 0 ? ( - - No active proposals - - Check back later for new governance proposals - - - ) : ( - proposals.map((proposal) => ( - { - setSelectedProposal(proposal); - setVoteSheetVisible(true); - }} - /> - )) + {/* Tab Content */} + {activeTab === 'proposals' && ( + <> + Active Proposals + {proposals.length === 0 ? ( + + No active proposals + + Check back later for new governance proposals + + + ) : ( + proposals.map((proposal) => ( + { + setSelectedProposal(proposal); + setVoteSheetVisible(true); + }} + /> + )) + )} + + )} + + {activeTab === 'elections' && ( + <> + Active Elections + {elections.length === 0 ? ( + + No active elections + + Check back later for upcoming elections + + + ) : ( + elections.map((election) => ( + { + setSelectedElection(election); + setVotedCandidates([]); + setElectionSheetVisible(true); + }} + /> + )) + )} + + )} + + {activeTab === 'parliament' && ( + <> + Parliament Status + + + Active Members + 0 / 27 + + + Current Session + + + + Pending Votes + 5 + + + + Dรฎwan (Constitutional Court) + + + Active Judges + 0 / 9 + + + Pending Reviews + 3 + + + Recent Decisions + 12 + + + )} {/* Info Card */} @@ -265,10 +468,148 @@ export default function GovernanceScreen() { )} + + {/* Election Vote Bottom Sheet */} + setElectionSheetVisible(false)} + title={selectedElection ? `${selectedElection.type} Election` : 'Election'} + height={600} + > + {selectedElection && ( + + + + + {selectedElection.totalVotes.toLocaleString()} votes cast + + + + + {selectedElection.type === 'Parliamentary' + ? 'You can select multiple candidates' + : 'Select one candidate'} + + + {/* Candidates List */} + + {selectedElection.candidates.map((candidate) => ( + handleElectionVote(candidate.id)} + > + + + {candidate.name} + {candidate.party && ( + {candidate.party} + )} + + Trust Score: {candidate.trustScore} + + + + + {candidate.percentage.toFixed(1)}% + + + {candidate.votes.toLocaleString()} votes + + + + + + + {votedCandidates.includes(candidate.id) && ( + + โœ“ Selected + + )} + + ))} + + + {/* Submit Vote Button */} +