mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-13 09:01:00 +00:00
feat: Integrate real-time blockchain roles with Pallet-Tiki
Implemented comprehensive tiki (role) system integration with Dashboard: New Features: - Created src/lib/tiki.ts utility library based on real pallet-tiki implementation - Added fetchUserTikis() to query user roles from blockchain - Implemented role scoring, categorization, and display helpers - Added "Roles & Tikis" tab to Dashboard showing: * Primary role from highest-scoring tiki * Total tiki score (sum of all role scores) * All assigned roles with emoji indicators * Role categories (Government, Judiciary, Education, etc.) * Connected wallet address Role System Details: - 49 different tiki types from pallet-tiki (Serok, Wezir, Dadger, etc.) - Role assignment types: Automatic, Appointed, Elected, Earned - Score-based hierarchy (10-250 points per role) - Dynamic role display based on blockchain state - Fallback to "Member" when no wallet connected or no tikis assigned Dashboard Improvements: - Fixed email verification using Supabase auth.resend() - Fixed Edit Profile button to navigate to /profile/settings - Added Tiki Score card showing total score and role count - Expanded stats grid to 4 columns (Status, Join Date, Role, Tiki Score) - Real-time role updates when wallet connects/disconnects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+400
@@ -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<string, string> = {
|
||||||
|
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<string, number> = {
|
||||||
|
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<string, string[]> = {
|
||||||
|
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<string[]> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
+155
-10
@@ -5,20 +5,29 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
import { supabase } from '@/lib/supabase';
|
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 { useToast } from '@/hooks/use-toast';
|
||||||
|
import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories } from '@/lib/tiki';
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { api, isApiReady, selectedAccount } = usePolkadot();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [profile, setProfile] = useState<any>(null);
|
const [profile, setProfile] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [tikis, setTikis] = useState<string[]>([]);
|
||||||
|
const [tikiScore, setTikiScore] = useState<number>(0);
|
||||||
|
const [loadingTikis, setLoadingTikis] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
}, [user]);
|
if (selectedAccount && api && isApiReady) {
|
||||||
|
fetchTikiData();
|
||||||
|
}
|
||||||
|
}, [user, selectedAccount, api, isApiReady]);
|
||||||
|
|
||||||
const fetchProfile = async () => {
|
const fetchProfile = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@@ -39,10 +48,28 @@ 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 () => {
|
const sendVerificationEmail = async () => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase.functions.invoke('email-verification', {
|
// Supabase automatically sends verification email when updating email
|
||||||
body: { action: 'send', email: user?.email }
|
// or we can trigger resend with updateUser
|
||||||
|
const { error } = await supabase.auth.resend({
|
||||||
|
type: 'signup',
|
||||||
|
email: user?.email || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
@@ -51,15 +78,30 @@ export default function Dashboard() {
|
|||||||
title: "Verification Email Sent",
|
title: "Verification Email Sent",
|
||||||
description: "Please check your email for verification link",
|
description: "Please check your email for verification link",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
console.error('Error sending verification email:', error);
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to send verification email",
|
description: error.message || "Failed to send verification email",
|
||||||
variant: "destructive"
|
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) {
|
if (loading) {
|
||||||
return <div className="flex justify-center items-center h-screen">Loading...</div>;
|
return <div className="flex justify-center items-center h-screen">Loading...</div>;
|
||||||
}
|
}
|
||||||
@@ -74,7 +116,7 @@ export default function Dashboard() {
|
|||||||
</button>
|
</button>
|
||||||
<h1 className="text-3xl font-bold mb-6">User Dashboard</h1>
|
<h1 className="text-3xl font-bold mb-6">User Dashboard</h1>
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-3 mb-6">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4 mb-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">Account Status</CardTitle>
|
<CardTitle className="text-sm font-medium">Account Status</CardTitle>
|
||||||
@@ -116,10 +158,25 @@ export default function Dashboard() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">
|
<div className="text-2xl font-bold">
|
||||||
{profile?.role || 'Member'}
|
{getRoleDisplay()}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Account type
|
{selectedAccount ? 'From Tiki NFTs' : 'Connect wallet for roles'}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Tiki Score</CardTitle>
|
||||||
|
<Award className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{loadingTikis ? '...' : tikiScore}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{tikis.length} {tikis.length === 1 ? 'role' : 'roles'} assigned
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -128,6 +185,7 @@ export default function Dashboard() {
|
|||||||
<Tabs defaultValue="profile" className="space-y-4">
|
<Tabs defaultValue="profile" className="space-y-4">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="profile">Profile</TabsTrigger>
|
<TabsTrigger value="profile">Profile</TabsTrigger>
|
||||||
|
<TabsTrigger value="roles">Roles & Tikis</TabsTrigger>
|
||||||
<TabsTrigger value="security">Security</TabsTrigger>
|
<TabsTrigger value="security">Security</TabsTrigger>
|
||||||
<TabsTrigger value="activity">Activity</TabsTrigger>
|
<TabsTrigger value="activity">Activity</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -179,7 +237,94 @@ export default function Dashboard() {
|
|||||||
<span>{profile?.location || 'Not set'}</span>
|
<span>{profile?.location || 'Not set'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => navigate('/settings')}>Edit Profile</Button>
|
<Button onClick={() => navigate('/profile/settings')}>Edit Profile</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="roles" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Roles & Tikis</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{selectedAccount
|
||||||
|
? 'Your roles from the blockchain (Pallet-Tiki)'
|
||||||
|
: 'Connect your wallet to view your roles'}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{!selectedAccount && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Shield className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
Connect your Polkadot wallet to view your on-chain roles
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => navigate('/')}>
|
||||||
|
Go to Home to Connect Wallet
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedAccount && loadingTikis && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-muted-foreground">Loading roles from blockchain...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedAccount && !loadingTikis && tikis.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Award className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<p className="text-muted-foreground mb-2">
|
||||||
|
No roles assigned yet
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Complete KYC to become a Citizen (Hemwelatî)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedAccount && !loadingTikis && tikis.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">Primary Role:</span>
|
||||||
|
<Badge className="text-lg">
|
||||||
|
{getTikiEmoji(getPrimaryRole(tikis))} {getTikiDisplayName(getPrimaryRole(tikis))}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">Total Score:</span>
|
||||||
|
<span className="text-lg font-bold text-purple-600">{tikiScore}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">Categories:</span>
|
||||||
|
<span className="text-muted-foreground">{getRoleCategories().join(', ')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-4">
|
||||||
|
<h4 className="font-medium mb-3">All Roles ({tikis.length})</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{tikis.map((tiki, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="outline"
|
||||||
|
className={getTikiColor(tiki)}
|
||||||
|
>
|
||||||
|
{getTikiEmoji(tiki)} {getTikiDisplayName(tiki)}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-4 bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||||
|
<h4 className="font-medium mb-2">Blockchain Address</h4>
|
||||||
|
<p className="text-sm text-muted-foreground font-mono break-all">
|
||||||
|
{selectedAccount.address}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user