diff --git a/src/lib/tiki.ts b/src/lib/tiki.ts new file mode 100644 index 00000000..f405de2a --- /dev/null +++ b/src/lib/tiki.ts @@ -0,0 +1,400 @@ +// ======================================== +// Pallet-Tiki Integration +// ======================================== +// This file handles all tiki-related blockchain interactions +// Based on: /Pezkuwi-SDK/pezkuwi/pallets/tiki/src/lib.rs + +import type { ApiPromise } from '@polkadot/api'; + +// ======================================== +// TIKI TYPES (from Rust enum) +// ======================================== +export enum Tiki { + // Otomatik - KYC sonrası + Hemwelatî = 'Hemwelatî', + + // Seçilen roller (Elected) + Parlementer = 'Parlementer', + SerokiMeclise = 'SerokiMeclise', + Serok = 'Serok', + + // Atanan roller (Appointed) - Yargı + EndameDiwane = 'EndameDiwane', + Dadger = 'Dadger', + Dozger = 'Dozger', + Hiquqnas = 'Hiquqnas', + Noter = 'Noter', + + // Atanan roller - Yürütme + Wezir = 'Wezir', + SerokWeziran = 'SerokWeziran', + WezireDarayiye = 'WezireDarayiye', + WezireParez = 'WezireParez', + WezireDad = 'WezireDad', + WezireBelaw = 'WezireBelaw', + WezireTend = 'WezireTend', + WezireAva = 'WezireAva', + WezireCand = 'WezireCand', + + // Atanan roller - İdari + Xezinedar = 'Xezinedar', + Bacgir = 'Bacgir', + GerinendeyeCavkaniye = 'GerinendeyeCavkaniye', + OperatorêTorê = 'OperatorêTorê', + PisporêEwlehiyaSîber = 'PisporêEwlehiyaSîber', + GerinendeyeDaneye = 'GerinendeyeDaneye', + Berdevk = 'Berdevk', + Qeydkar = 'Qeydkar', + Balyoz = 'Balyoz', + Navbeynkar = 'Navbeynkar', + ParêzvaneÇandî = 'ParêzvaneÇandî', + Mufetîs = 'Mufetîs', + KalîteKontrolker = 'KalîteKontrolker', + + // Atanan roller - Kültürel/Dini + Mela = 'Mela', + Feqî = 'Feqî', + Perwerdekar = 'Perwerdekar', + Rewsenbîr = 'Rewsenbîr', + RêveberêProjeyê = 'RêveberêProjeyê', + SerokêKomele = 'SerokêKomele', + ModeratorêCivakê = 'ModeratorêCivakê', + + // Kazanılan roller (Earned) + Axa = 'Axa', + Pêseng = 'Pêseng', + Sêwirmend = 'Sêwirmend', + Hekem = 'Hekem', + Mamoste = 'Mamoste', + + // Ekonomik rol + Bazargan = 'Bazargan', +} + +// Role assignment types +export enum RoleAssignmentType { + Automatic = 'Automatic', + Appointed = 'Appointed', + Elected = 'Elected', + Earned = 'Earned', +} + +// Tiki to Display Name mapping (English) +export const TIKI_DISPLAY_NAMES: Record = { + Hemwelatî: 'Citizen', + Parlementer: 'Parliament Member', + SerokiMeclise: 'Speaker of Parliament', + Serok: 'President', + Wezir: 'Minister', + SerokWeziran: 'Prime Minister', + WezireDarayiye: 'Minister of Finance', + WezireParez: 'Minister of Defense', + WezireDad: 'Minister of Justice', + WezireBelaw: 'Minister of Education', + WezireTend: 'Minister of Health', + WezireAva: 'Minister of Water', + WezireCand: 'Minister of Culture', + EndameDiwane: 'Supreme Court Member', + Dadger: 'Judge', + Dozger: 'Prosecutor', + Hiquqnas: 'Lawyer', + Noter: 'Notary', + Xezinedar: 'Treasurer', + Bacgir: 'Tax Collector', + GerinendeyeCavkaniye: 'Resource Manager', + OperatorêTorê: 'Network Operator', + PisporêEwlehiyaSîber: 'Cybersecurity Expert', + GerinendeyeDaneye: 'Data Manager', + Berdevk: 'Representative', + Qeydkar: 'Registrar', + Balyoz: 'Ambassador', + Navbeynkar: 'Mediator', + ParêzvaneÇandî: 'Cultural Guardian', + Mufetîs: 'Inspector', + KalîteKontrolker: 'Quality Controller', + Mela: 'Religious Scholar', + Feqî: 'Religious Jurist', + Perwerdekar: 'Educator', + Rewsenbîr: 'Intellectual', + RêveberêProjeyê: 'Project Manager', + SerokêKomele: 'Community Leader', + ModeratorêCivakê: 'Community Moderator', + Axa: 'Elder', + Pêseng: 'Pioneer', + Sêwirmend: 'Advisor', + Hekem: 'Expert', + Mamoste: 'Teacher', + Bazargan: 'Merchant', +}; + +// Tiki scores (from get_bonus_for_tiki function) +export const TIKI_SCORES: Record = { + Axa: 250, + RêveberêProjeyê: 250, + ModeratorêCivakê: 200, + Serok: 200, + EndameDiwane: 175, + Dadger: 150, + SerokiMeclise: 150, + SerokWeziran: 125, + Dozger: 120, + Serok: 100, + Wezir: 100, + WezireDarayiye: 100, + WezireParez: 100, + WezireDad: 100, + WezireBelaw: 100, + WezireTend: 100, + WezireAva: 100, + WezireCand: 100, + SerokêKomele: 100, + Xezinedar: 100, + PisporêEwlehiyaSîber: 100, + Parlementer: 100, + Mufetîs: 90, + Balyoz: 80, + Hiquqnas: 75, + Berdevk: 70, + Mamoste: 70, + Bazargan: 60, + OperatorêTorê: 60, + Mela: 50, + Feqî: 50, + Noter: 50, + Bacgir: 50, + Perwerdekar: 40, + Rewsenbîr: 40, + GerinendeyeCavkaniye: 40, + GerinendeyeDaneye: 40, + KalîteKontrolker: 30, + Navbeynkar: 30, + Hekem: 30, + Qeydkar: 25, + ParêzvaneÇandî: 25, + Sêwirmend: 20, + Hemwelatî: 10, + Pêseng: 5, // Default for unlisted +}; + +// ======================================== +// ROLE CATEGORIZATION +// ======================================== + +export const ROLE_CATEGORIES: Record = { + Government: ['Serok', 'SerokWeziran', 'Wezir', 'WezireDarayiye', 'WezireParez', 'WezireDad', 'WezireBelaw', 'WezireTend', 'WezireAva', 'WezireCand'], + Legislature: ['Parlementer', 'SerokiMeclise'], + Judiciary: ['EndameDiwane', 'Dadger', 'Dozger', 'Hiquqnas', 'Noter'], + Administration: ['Xezinedar', 'Bacgir', 'Berdevk', 'Qeydkar', 'Balyoz', 'Mufetîs'], + Technical: ['OperatorêTorê', 'PisporêEwlehiyaSîber', 'GerinendeyeDaneye', 'GerinendeyeCavkaniye'], + Cultural: ['Mela', 'Feqî', 'ParêzvaneÇandî'], + Education: ['Mamoste', 'Perwerdekar', 'Rewsenbîr'], + Community: ['SerokêKomele', 'ModeratorêCivakê', 'Axa', 'Navbeynkar', 'Sêwirmend', 'Hekem'], + Economic: ['Bazargan'], + Leadership: ['RêveberêProjeyê', 'Pêseng'], + Quality: ['KalîteKontrolker'], + Citizen: ['Hemwelatî'], +}; + +// ======================================== +// TIKI QUERY FUNCTIONS +// ======================================== + +/** + * Fetch user's tiki roles from blockchain + * @param api - Polkadot API instance + * @param address - User's substrate address + * @returns Array of tiki role strings + */ +export const fetchUserTikis = async ( + api: ApiPromise, + address: string +): Promise => { + try { + if (!api || !api.query.tiki) { + console.warn('Tiki pallet not available on this chain'); + return []; + } + + // Query UserTikis storage + const tikis = await api.query.tiki.userTikis(address); + const tikisArray = tikis.toJSON() as any[]; + + if (!tikisArray || tikisArray.length === 0) { + return []; + } + + // Convert from enum index to string names + return tikisArray.map((tikiIndex: any) => { + // Tikis are stored as enum variants + if (typeof tikiIndex === 'string') { + return tikiIndex; + } else if (typeof tikiIndex === 'object' && tikiIndex !== null) { + // Handle object variant format {variantName: null} + return Object.keys(tikiIndex)[0]; + } + return 'Unknown'; + }).filter((t: string) => t !== 'Unknown'); + + } catch (error) { + console.error('Error fetching user tikis:', error); + return []; + } +}; + +/** + * Check if user is a citizen (has Hemwelatî tiki) + * @param api - Polkadot API instance + * @param address - User's substrate address + * @returns boolean + */ +export const isCitizen = async ( + api: ApiPromise, + address: string +): Promise => { + try { + if (!api || !api.query.tiki) { + return false; + } + + const citizenNft = await api.query.tiki.citizenNft(address); + return !citizenNft.isEmpty; + } catch (error) { + console.error('Error checking citizenship:', error); + return false; + } +}; + +/** + * Calculate total tiki score for a user + * @param tikis - Array of tiki strings + * @returns Total score + */ +export const calculateTikiScore = (tikis: string[]): number => { + return tikis.reduce((total, tiki) => { + return total + (TIKI_SCORES[tiki] || 5); + }, 0); +}; + +/** + * Get primary role (highest scoring) from tikis + * @param tikis - Array of tiki strings + * @returns Primary role string + */ +export const getPrimaryRole = (tikis: string[]): string => { + if (!tikis || tikis.length === 0) { + return 'Member'; + } + + // Find highest scoring role + let primaryRole = tikis[0]; + let highestScore = TIKI_SCORES[tikis[0]] || 5; + + for (const tiki of tikis) { + const score = TIKI_SCORES[tiki] || 5; + if (score > highestScore) { + highestScore = score; + primaryRole = tiki; + } + } + + return primaryRole; +}; + +/** + * Get display name for a tiki + * @param tiki - Tiki string + * @returns Display name + */ +export const getTikiDisplayName = (tiki: string): string => { + return TIKI_DISPLAY_NAMES[tiki] || tiki; +}; + +/** + * Get all role categories for user's tikis + * @param tikis - Array of tiki strings + * @returns Array of category names + */ +export const getUserRoleCategories = (tikis: string[]): string[] => { + const categories = new Set(); + + for (const tiki of tikis) { + for (const [category, roles] of Object.entries(ROLE_CATEGORIES)) { + if (roles.includes(tiki)) { + categories.add(category); + } + } + } + + return Array.from(categories); +}; + +/** + * Check if user has a specific tiki + * @param tikis - Array of tiki strings + * @param tiki - Tiki to check + * @returns boolean + */ +export const hasTiki = (tikis: string[], tiki: string): boolean => { + return tikis.includes(tiki); +}; + +// ======================================== +// DISPLAY HELPERS +// ======================================== + +/** + * Get color for a tiki role + * @param tiki - Tiki string + * @returns Tailwind color class + */ +export const getTikiColor = (tiki: string): string => { + const score = TIKI_SCORES[tiki] || 5; + + if (score >= 200) return 'text-purple-500'; + if (score >= 150) return 'text-pink-500'; + if (score >= 100) return 'text-blue-500'; + if (score >= 70) return 'text-cyan-500'; + if (score >= 40) return 'text-teal-500'; + if (score >= 20) return 'text-green-500'; + return 'text-gray-500'; +}; + +/** + * Get emoji icon for a tiki category + * @param tiki - Tiki string + * @returns Emoji string + */ +export const getTikiEmoji = (tiki: string): string => { + for (const [category, roles] of Object.entries(ROLE_CATEGORIES)) { + if (roles.includes(tiki)) { + switch (category) { + case 'Government': return '👑'; + case 'Legislature': return '🏛️'; + case 'Judiciary': return '⚖️'; + case 'Administration': return '📋'; + case 'Technical': return '💻'; + case 'Cultural': return '📿'; + case 'Education': return '👨‍🏫'; + case 'Community': return '🤝'; + case 'Economic': return '💰'; + case 'Leadership': return '⭐'; + case 'Quality': return '✅'; + case 'Citizen': return '👤'; + } + } + } + return '👤'; +}; + +/** + * Get badge variant for a tiki + * @param tiki - Tiki string + * @returns Badge variant string + */ +export const getTikiBadgeVariant = (tiki: string): 'default' | 'secondary' | 'destructive' | 'outline' => { + const score = TIKI_SCORES[tiki] || 5; + + if (score >= 150) return 'default'; // Purple/blue for high ranks + if (score >= 70) return 'secondary'; // Gray for mid ranks + return 'outline'; // Outline for low ranks +}; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index a20492c0..05c170ef 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -5,20 +5,29 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useAuth } from '@/contexts/AuthContext'; +import { usePolkadot } from '@/contexts/PolkadotContext'; import { supabase } from '@/lib/supabase'; -import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowLeft } from 'lucide-react'; +import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowLeft, Award } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; +import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories } from '@/lib/tiki'; export default function Dashboard() { const { user } = useAuth(); + const { api, isApiReady, selectedAccount } = usePolkadot(); const navigate = useNavigate(); const { toast } = useToast(); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); + const [tikis, setTikis] = useState([]); + const [tikiScore, setTikiScore] = useState(0); + const [loadingTikis, setLoadingTikis] = useState(false); useEffect(() => { fetchProfile(); - }, [user]); + if (selectedAccount && api && isApiReady) { + fetchTikiData(); + } + }, [user, selectedAccount, api, isApiReady]); const fetchProfile = async () => { if (!user) return; @@ -39,27 +48,60 @@ export default function Dashboard() { } }; + const fetchTikiData = async () => { + if (!selectedAccount || !api) return; + + setLoadingTikis(true); + try { + const userTikis = await fetchUserTikis(api, selectedAccount.address); + setTikis(userTikis); + setTikiScore(calculateTikiScore(userTikis)); + } catch (error) { + console.error('Error fetching tiki data:', error); + } finally { + setLoadingTikis(false); + } + }; + const sendVerificationEmail = async () => { try { - const { data, error } = await supabase.functions.invoke('email-verification', { - body: { action: 'send', email: user?.email } + // Supabase automatically sends verification email when updating email + // or we can trigger resend with updateUser + const { error } = await supabase.auth.resend({ + type: 'signup', + email: user?.email || '', }); if (error) throw error; - + toast({ title: "Verification Email Sent", description: "Please check your email for verification link", }); - } catch (error) { + } catch (error: any) { + console.error('Error sending verification email:', error); toast({ title: "Error", - description: "Failed to send verification email", + description: error.message || "Failed to send verification email", variant: "destructive" }); } }; + const getRoleDisplay = (): string => { + if (loadingTikis) return 'Loading...'; + if (!selectedAccount) return 'Member'; + if (tikis.length === 0) return 'Member'; + + const primaryRole = getPrimaryRole(tikis); + return getTikiDisplayName(primaryRole); + }; + + const getRoleCategories = (): string[] => { + if (tikis.length === 0) return ['Member']; + return getUserRoleCategories(tikis); + }; + if (loading) { return
Loading...
; } @@ -74,7 +116,7 @@ export default function Dashboard() {

User Dashboard

-
+
Account Status @@ -116,10 +158,25 @@ export default function Dashboard() {
- {profile?.role || 'Member'} + {getRoleDisplay()}

- Account type + {selectedAccount ? 'From Tiki NFTs' : 'Connect wallet for roles'} +

+
+
+ + + + Tiki Score + + + +
+ {loadingTikis ? '...' : tikiScore} +
+

+ {tikis.length} {tikis.length === 1 ? 'role' : 'roles'} assigned

@@ -128,6 +185,7 @@ export default function Dashboard() { Profile + Roles & Tikis Security Activity @@ -179,7 +237,94 @@ export default function Dashboard() { {profile?.location || 'Not set'}
- + + + + + + + + + Roles & Tikis + + {selectedAccount + ? 'Your roles from the blockchain (Pallet-Tiki)' + : 'Connect your wallet to view your roles'} + + + + {!selectedAccount && ( +
+ +

+ Connect your Polkadot wallet to view your on-chain roles +

+ +
+ )} + + {selectedAccount && loadingTikis && ( +
+

Loading roles from blockchain...

+
+ )} + + {selectedAccount && !loadingTikis && tikis.length === 0 && ( +
+ +

+ No roles assigned yet +

+

+ Complete KYC to become a Citizen (Hemwelatî) +

+
+ )} + + {selectedAccount && !loadingTikis && tikis.length > 0 && ( +
+
+
+ Primary Role: + + {getTikiEmoji(getPrimaryRole(tikis))} {getTikiDisplayName(getPrimaryRole(tikis))} + +
+
+ Total Score: + {tikiScore} +
+
+ Categories: + {getRoleCategories().join(', ')} +
+
+ +
+

All Roles ({tikis.length})

+
+ {tikis.map((tiki, index) => ( + + {getTikiEmoji(tiki)} {getTikiDisplayName(tiki)} + + ))} +
+
+ +
+

Blockchain Address

+

+ {selectedAccount.address} +

+
+
+ )}