Reorganize repository into monorepo structure

Restructured the project to support multiple frontend applications:
- Move web app to web/ directory
- Create pezkuwi-sdk-ui/ for Polkadot SDK clone (planned)
- Create mobile/ directory for mobile app development
- Add shared/ directory with common utilities, types, and blockchain code
- Update README.md with comprehensive documentation
- Remove obsolete DKSweb/ directory

This monorepo structure enables better code sharing and organized
development across web, mobile, and SDK UI projects.
This commit is contained in:
Claude
2025-11-14 00:46:35 +00:00
parent bb3d9aeb29
commit c48ded7ff2
206 changed files with 502 additions and 4 deletions
+329
View File
@@ -0,0 +1,329 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/hooks/use-toast';
import { supabase } from '@/lib/supabase';
import { Users, Settings, Activity, Shield, Bell, Trash2, Monitor, Lock, AlertTriangle, ArrowLeft } from 'lucide-react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { SessionMonitor } from '@/components/security/SessionMonitor';
import { PermissionEditor } from '@/components/security/PermissionEditor';
import { SecurityAudit } from '@/components/security/SecurityAudit';
export default function AdminPanel() {
const navigate = useNavigate();
const [users, setUsers] = useState<any[]>([]);
const [adminRoles, setAdminRoles] = useState<any[]>([]);
const [systemSettings, setSystemSettings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
useEffect(() => {
loadAdminData();
}, []);
const loadAdminData = async () => {
try {
// Load users
const { data: profiles } = await supabase
.from('profiles')
.select('*')
.order('created_at', { ascending: false });
// Load admin roles
const { data: roles } = await supabase
.from('admin_roles')
.select('*');
// Load system settings
const { data: settings } = await supabase
.from('system_settings')
.select('*');
setUsers(profiles || []);
setAdminRoles(roles || []);
setSystemSettings(settings || []);
} catch (error) {
console.error('Error loading admin data:', error);
} finally {
setLoading(false);
}
};
const updateUserRole = async (userId: string, role: string) => {
try {
if (role === 'none') {
await supabase
.from('admin_roles')
.delete()
.eq('user_id', userId);
} else {
await supabase
.from('admin_roles')
.upsert({
user_id: userId,
role,
updated_at: new Date().toISOString()
});
}
toast({
title: 'Success',
description: 'User role updated successfully',
});
loadAdminData();
} catch (error) {
toast({
title: 'Error',
description: 'Failed to update user role',
variant: 'destructive',
});
}
};
const sendNotification = async (userId: string) => {
const title = prompt('Notification Title:');
const message = prompt('Notification Message:');
if (!title || !message) return;
try {
const { error } = await supabase.functions.invoke('notifications-manager', {
body: {
action: 'create',
userId,
title,
message,
type: 'info'
}
});
if (error) throw error;
toast({
title: 'Success',
description: 'Notification sent successfully',
});
} catch (error) {
toast({
title: 'Error',
description: 'Failed to send notification',
variant: 'destructive',
});
}
};
const getUserRole = (userId: string) => {
const role = adminRoles.find(r => r.user_id === userId);
return role?.role || 'none';
};
if (loading) {
return <div className="flex justify-center items-center h-screen">Loading...</div>;
}
return (
<div className="container mx-auto py-8 relative">
<button
onClick={() => navigate('/')}
className="absolute top-4 left-4 text-gray-400 hover:text-white transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<h1 className="text-3xl font-bold mb-8">Admin Panel</h1>
<Tabs defaultValue="users" className="space-y-4">
<TabsList className="grid w-full grid-cols-7">
<TabsTrigger value="users">
<Users className="mr-2 h-4 w-4" />
Users
</TabsTrigger>
<TabsTrigger value="roles">
<Shield className="mr-2 h-4 w-4" />
Roles
</TabsTrigger>
<TabsTrigger value="sessions">
<Monitor className="mr-2 h-4 w-4" />
Sessions
</TabsTrigger>
<TabsTrigger value="permissions">
<Lock className="mr-2 h-4 w-4" />
Permissions
</TabsTrigger>
<TabsTrigger value="security">
<AlertTriangle className="mr-2 h-4 w-4" />
Security
</TabsTrigger>
<TabsTrigger value="activity">
<Activity className="mr-2 h-4 w-4" />
Activity
</TabsTrigger>
<TabsTrigger value="settings">
<Settings className="mr-2 h-4 w-4" />
Settings
</TabsTrigger>
</TabsList>
<TabsContent value="users">
<Card>
<CardHeader>
<CardTitle>User Management</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Username</TableHead>
<TableHead>Email</TableHead>
<TableHead>Verified</TableHead>
<TableHead>Role</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.username}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge variant={user.email_verified ? 'default' : 'secondary'}>
{user.email_verified ? 'Verified' : 'Unverified'}
</Badge>
</TableCell>
<TableCell>
<Select
value={getUserRole(user.id)}
onValueChange={(value) => updateUserRole(user.id, value)}
>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">User</SelectItem>
<SelectItem value="moderator">Moderator</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="super_admin">Super Admin</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => sendNotification(user.id)}
>
<Bell className="h-4 w-4 mr-1" />
Notify
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="roles">
<Card>
<CardHeader>
<CardTitle>Admin Roles</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{adminRoles.map((role) => {
const user = users.find(u => u.id === role.user_id);
return (
<div key={role.id} className="flex items-center justify-between p-4 border rounded">
<div>
<p className="font-medium">{user?.username}</p>
<p className="text-sm text-muted-foreground">{user?.email}</p>
</div>
<Badge variant="outline">{role.role}</Badge>
</div>
);
})}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="sessions">
<SessionMonitor />
</TabsContent>
<TabsContent value="permissions">
<PermissionEditor />
</TabsContent>
<TabsContent value="security">
<SecurityAudit />
</TabsContent>
<TabsContent value="activity">
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">Activity logs will be displayed here</p>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="settings">
<Card>
<CardHeader>
<CardTitle>System Settings</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<Label>Maintenance Mode</Label>
<Select defaultValue="off">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="off">Off</SelectItem>
<SelectItem value="on">On</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Registration</Label>
<Select defaultValue="open">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
<SelectItem value="invite">Invite Only</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}
+206
View File
@@ -0,0 +1,206 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { CitizenshipModal } from '@/components/citizenship/CitizenshipModal';
import { Shield, Users, Award, Globe, ChevronRight, ArrowLeft, Home } from 'lucide-react';
const BeCitizen: React.FC = () => {
const navigate = useNavigate();
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-900 to-blue-900">
<div className="container mx-auto px-4 py-16">
{/* Back to Home Button */}
<div className="mb-8">
<Button
onClick={() => navigate('/')}
variant="outline"
className="bg-white/10 hover:bg-white/20 border-white/30 text-white"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Home
</Button>
</div>
{/* Hero Section */}
<div className="text-center mb-16">
<h1 className="text-5xl md:text-6xl font-bold text-white mb-6">
🏛 Digital Kurdistan
</h1>
<h2 className="text-3xl md:text-4xl font-semibold text-cyan-300 mb-4">
Bibe Welati / Be a Citizen
</h2>
<p className="text-xl text-gray-200 max-w-3xl mx-auto">
Join the Digital Kurdistan State as a sovereign citizen. Receive your Welati Tiki NFT and unlock governance, trust scoring, and community benefits.
</p>
</div>
{/* Benefits Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
<Card className="bg-white/10 backdrop-blur-md border-cyan-500/30 hover:border-cyan-500 transition-all">
<CardHeader>
<Shield className="h-12 w-12 text-cyan-400 mb-3" />
<CardTitle className="text-white">Privacy Protected</CardTitle>
<CardDescription className="text-gray-300">
Your data is encrypted with ZK-proofs. Only hashes are stored on-chain.
</CardDescription>
</CardHeader>
</Card>
<Card className="bg-white/10 backdrop-blur-md border-purple-500/30 hover:border-purple-500 transition-all">
<CardHeader>
<Award className="h-12 w-12 text-purple-400 mb-3" />
<CardTitle className="text-white">Welati Tiki NFT</CardTitle>
<CardDescription className="text-gray-300">
Receive your unique soulbound citizenship NFT after KYC approval.
</CardDescription>
</CardHeader>
</Card>
<Card className="bg-white/10 backdrop-blur-md border-green-500/30 hover:border-green-500 transition-all">
<CardHeader>
<Users className="h-12 w-12 text-green-400 mb-3" />
<CardTitle className="text-white">Trust Scoring</CardTitle>
<CardDescription className="text-gray-300">
Build trust through referrals, staking, and community contributions.
</CardDescription>
</CardHeader>
</Card>
<Card className="bg-white/10 backdrop-blur-md border-yellow-500/30 hover:border-yellow-500 transition-all">
<CardHeader>
<Globe className="h-12 w-12 text-yellow-400 mb-3" />
<CardTitle className="text-white">Governance Access</CardTitle>
<CardDescription className="text-gray-300">
Participate in on-chain governance and shape the future of Digital Kurdistan.
</CardDescription>
</CardHeader>
</Card>
</div>
{/* CTA Section */}
<div className="max-w-4xl mx-auto">
<Card className="bg-white/5 backdrop-blur-lg border-cyan-500/50">
<CardContent className="pt-8 pb-8">
<div className="text-center space-y-6">
<div>
<h3 className="text-2xl font-bold text-white mb-3">Ready to Join?</h3>
<p className="text-gray-300 mb-6">
Whether you're already a citizen or want to become one, start your journey here.
</p>
</div>
<Button
onClick={() => setIsModalOpen(true)}
size="lg"
className="bg-gradient-to-r from-cyan-500 to-purple-600 hover:from-cyan-600 hover:to-purple-700 text-white font-semibold px-8 py-6 text-lg group"
>
<span>Start Citizenship Process</span>
<ChevronRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</Button>
<div className="flex flex-col md:flex-row gap-4 justify-center items-center text-sm text-gray-400 pt-4">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4" />
<span>Secure ZK-Proof Authentication</span>
</div>
<div className="hidden md:block"></div>
<div className="flex items-center gap-2">
<Award className="h-4 w-4" />
<span>Soulbound NFT Citizenship</span>
</div>
<div className="hidden md:block"></div>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4" />
<span>Decentralized Identity</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Process Overview */}
<div className="mt-16 max-w-5xl mx-auto">
<h3 className="text-3xl font-bold text-white text-center mb-8">How It Works</h3>
<div className="grid md:grid-cols-3 gap-8">
{/* Existing Citizens */}
<Card className="bg-white/5 backdrop-blur-md border-cyan-500/30">
<CardHeader>
<div className="bg-cyan-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-cyan-400">1</span>
</div>
<CardTitle className="text-white">Already a Citizen?</CardTitle>
</CardHeader>
<CardContent className="text-gray-300 space-y-2 text-sm">
<p> Enter your Welati Tiki NFT number</p>
<p> Verify NFT ownership on-chain</p>
<p> Sign authentication challenge</p>
<p> Access your citizen dashboard</p>
</CardContent>
</Card>
{/* New Citizens */}
<Card className="bg-white/5 backdrop-blur-md border-purple-500/30">
<CardHeader>
<div className="bg-purple-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-purple-400">2</span>
</div>
<CardTitle className="text-white">New to Citizenship?</CardTitle>
</CardHeader>
<CardContent className="text-gray-300 space-y-2 text-sm">
<p> Fill detailed KYC application</p>
<p> Data encrypted with ZK-proofs</p>
<p> Submit for admin approval</p>
<p> Receive your Welati Tiki NFT</p>
</CardContent>
</Card>
{/* After Citizenship */}
<Card className="bg-white/5 backdrop-blur-md border-green-500/30">
<CardHeader>
<div className="bg-green-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-green-400">3</span>
</div>
<CardTitle className="text-white">Citizen Benefits</CardTitle>
</CardHeader>
<CardContent className="text-gray-300 space-y-2 text-sm">
<p> Trust score calculation enabled</p>
<p> Governance voting rights</p>
<p> Referral tree participation</p>
<p> Staking multiplier bonuses</p>
</CardContent>
</Card>
</div>
</div>
{/* Security Notice */}
<div className="mt-12 max-w-3xl mx-auto">
<Card className="bg-yellow-500/10 border-yellow-500/30">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<Shield className="h-6 w-6 text-yellow-400 mt-1 flex-shrink-0" />
<div className="text-sm text-gray-200">
<p className="font-semibold text-yellow-400 mb-2">Privacy & Security</p>
<p>
Your personal data is encrypted using AES-GCM with your wallet-derived keys.
Only commitment hashes are stored on the blockchain. Encrypted data is stored
on IPFS and locally on your device. No personal information is ever publicly visible.
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Citizenship Modal */}
<CitizenshipModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
</div>
);
};
export default BeCitizen;
+532
View File
@@ -0,0 +1,532 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
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, Award, Users, TrendingUp } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories } from '@/lib/tiki';
import { getAllScores, type UserScores } from '@/lib/scores';
export default function Dashboard() {
const { user } = useAuth();
const { api, isApiReady, selectedAccount } = usePolkadot();
const navigate = useNavigate();
const { toast } = useToast();
const [profile, setProfile] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [tikis, setTikis] = useState<string[]>([]);
const [scores, setScores] = useState<UserScores>({
trustScore: 0,
referralScore: 0,
stakingScore: 0,
tikiScore: 0,
totalScore: 0
});
const [loadingScores, setLoadingScores] = useState(false);
useEffect(() => {
fetchProfile();
if (selectedAccount && api && isApiReady) {
fetchScoresAndTikis();
}
}, [user, selectedAccount, api, isApiReady]);
const fetchProfile = async () => {
if (!user) return;
try {
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.maybeSingle();
if (error) {
console.error('Profile fetch error:', error);
return;
}
// Auto-sync user metadata from Auth to profiles if missing
if (data) {
const needsUpdate: any = {};
// Sync full_name from Auth metadata if not set in profiles
if (!data.full_name && user.user_metadata?.full_name) {
needsUpdate.full_name = user.user_metadata.full_name;
}
// Sync phone from Auth metadata if not set in profiles
if (!data.phone_number && user.user_metadata?.phone) {
needsUpdate.phone_number = user.user_metadata.phone;
}
// Sync email if not set
if (!data.email && user.email) {
needsUpdate.email = user.email;
}
// If there are fields to update, update the profile
if (Object.keys(needsUpdate).length > 0) {
const { error: updateError } = await supabase
.from('profiles')
.update(needsUpdate)
.eq('id', user.id);
if (!updateError) {
// Update local state
Object.assign(data, needsUpdate);
}
}
}
// Note: Email verification is handled by Supabase Auth (user.email_confirmed_at)
// We don't store it in profiles table to avoid duplication
setProfile(data);
} catch (error) {
console.error('Error fetching profile:', error);
} finally {
setLoading(false);
}
};
const fetchScoresAndTikis = async () => {
if (!selectedAccount || !api) return;
setLoadingScores(true);
try {
// Fetch all scores from blockchain (includes trust, referral, staking, tiki)
const allScores = await getAllScores(api, selectedAccount.address);
setScores(allScores);
// Also fetch tikis separately for role display (needed for role details)
const userTikis = await fetchUserTikis(api, selectedAccount.address);
setTikis(userTikis);
} catch (error) {
console.error('Error fetching scores and tikis:', error);
} finally {
setLoadingScores(false);
}
};
const sendVerificationEmail = async () => {
if (!user?.email) {
toast({
title: "Error",
description: "No email address found",
variant: "destructive"
});
return;
}
console.log('🔄 Attempting to send verification email to:', user.email);
console.log('🔐 User object:', user);
try {
// Method 1: Try resend API
console.log('📧 Trying Supabase auth.resend()...');
const { error: resendError } = await supabase.auth.resend({
type: 'signup',
email: user.email,
});
if (resendError) {
console.error('❌ Resend error:', resendError);
} else {
console.log('✅ Resend successful');
}
// If resend fails, try alternative method
if (resendError) {
console.warn('Resend failed, trying alternative method:', resendError);
// Method 2: Request password reset as verification alternative
// This will send an email if the account exists
const { error: resetError } = await supabase.auth.resetPasswordForEmail(user.email, {
redirectTo: `${window.location.origin}/email-verification`,
});
if (resetError) throw resetError;
}
toast({
title: "Verification Email Sent",
description: "Please check your email inbox and spam folder",
});
} catch (error: any) {
console.error('Error sending verification email:', error);
// Provide more detailed error message
let errorMessage = "Failed to send verification email";
if (error.message?.includes('Email rate limit exceeded')) {
errorMessage = "Too many requests. Please wait a few minutes and try again.";
} else if (error.message?.includes('User not found')) {
errorMessage = "Account not found. Please sign up first.";
} else if (error.message) {
errorMessage = error.message;
}
toast({
title: "Error",
description: errorMessage,
variant: "destructive"
});
}
};
const getRoleDisplay = (): string => {
if (loadingScores) 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 <div className="flex justify-center items-center h-screen">Loading...</div>;
}
return (
<div className="container mx-auto p-6 max-w-6xl relative">
<button
onClick={() => navigate('/')}
className="absolute top-4 left-4 text-gray-400 hover:text-white transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<h1 className="text-3xl font-bold mb-6">User Dashboard</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Account Status</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{user?.email_confirmed_at || profile?.email_verified ? (
<Badge className="bg-green-500">Verified</Badge>
) : (
<Badge variant="destructive">Unverified</Badge>
)}
</div>
<p className="text-xs text-muted-foreground">
{user?.email_confirmed_at
? `Verified on ${new Date(user.email_confirmed_at).toLocaleDateString()}`
: 'Email verification status'}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Member Since</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{new Date(profile?.joined_at || user?.created_at).toLocaleDateString()}
</div>
<p className="text-xs text-muted-foreground">
Registration date
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Role</CardTitle>
<User className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{getRoleDisplay()}
</div>
<p className="text-xs text-muted-foreground">
{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">Total Score</CardTitle>
<Award className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{loadingScores ? '...' : scores.totalScore}
</div>
<p className="text-xs text-muted-foreground">
Combined from all score types
</p>
</CardContent>
</Card>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Trust Score</CardTitle>
<Shield className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">
{loadingScores ? '...' : scores.trustScore}
</div>
<p className="text-xs text-muted-foreground">
From pallet_trust
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Referral Score</CardTitle>
<Users className="h-4 w-4 text-cyan-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-cyan-600">
{loadingScores ? '...' : scores.referralScore}
</div>
<p className="text-xs text-muted-foreground">
From referral system
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Staking Score</CardTitle>
<TrendingUp className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{loadingScores ? '...' : scores.stakingScore}
</div>
<p className="text-xs text-muted-foreground">
From pallet_staking_score
</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-pink-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-pink-600">
{loadingScores ? '...' : scores.tikiScore}
</div>
<p className="text-xs text-muted-foreground">
{tikis.length} {tikis.length === 1 ? 'role' : 'roles'} assigned
</p>
</CardContent>
</Card>
</div>
<Tabs defaultValue="profile" className="space-y-4">
<TabsList>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="roles">Roles & Tikis</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
</TabsList>
<TabsContent value="profile" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>Your personal details and contact information</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Full Name:</span>
<span>{profile?.full_name || 'Not set'}</span>
</div>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Email:</span>
<span>{user?.email}</span>
{user?.email_confirmed_at || profile?.email_verified ? (
<Badge className="bg-green-500">Verified</Badge>
) : (
<Button size="sm" variant="outline" onClick={sendVerificationEmail}>
Verify Email
</Button>
)}
</div>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Recovery Email:</span>
<span>{profile?.recovery_email || 'Not set'}</span>
{profile?.recovery_email_verified && profile?.recovery_email && (
<Badge className="bg-green-500">Verified</Badge>
)}
</div>
<div className="flex items-center gap-2">
<Phone className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Phone:</span>
<span>{profile?.phone_number || 'Not set'}</span>
</div>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Website:</span>
<span>{profile?.website || 'Not set'}</span>
</div>
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Location:</span>
<span>{profile?.location || 'Not set'}</span>
</div>
</div>
<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 && loadingScores && (
<div className="text-center py-8">
<p className="text-muted-foreground">Loading roles from blockchain...</p>
</div>
)}
{selectedAccount && !loadingScores && 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 && !loadingScores && 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">{scores.totalScore}</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>
</Card>
</TabsContent>
<TabsContent value="security" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Security Settings</CardTitle>
<CardDescription>Manage your account security</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h3 className="font-medium">Password</h3>
<p className="text-sm text-muted-foreground">Last changed: Never</p>
<Button onClick={() => navigate('/reset-password')}>Change Password</Button>
</div>
{!user?.email_confirmed_at && !profile?.email_verified && (
<div className="border-l-4 border-yellow-500 bg-yellow-50 p-4 text-gray-900">
<div className="flex items-center">
<AlertCircle className="h-5 w-5 text-yellow-600 mr-2" />
<div>
<h4 className="font-medium text-gray-900">Verify your email</h4>
<p className="text-sm text-gray-900">Please verify your email to access all features</p>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="activity" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>Your recent actions and transactions</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">No recent activity</p>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}
+96
View File
@@ -0,0 +1,96 @@
import { useEffect, useState } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { supabase } from '@/lib/supabase';
import { CheckCircle, XCircle, Loader2, ArrowLeft } from 'lucide-react';
export default function EmailVerification() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [verifying, setVerifying] = useState(true);
const [verified, setVerified] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
const token = searchParams.get('token');
if (token) {
verifyEmail(token);
} else {
setError('No verification token provided');
setVerifying(false);
}
}, [searchParams]);
const verifyEmail = async (token: string) => {
try {
const { data, error } = await supabase.functions.invoke('email-verification', {
body: { action: 'verify', token }
});
if (error) throw error;
setVerified(true);
} catch (err: any) {
setError(err.message || 'Failed to verify email');
} finally {
setVerifying(false);
}
};
return (
<div className="container mx-auto flex items-center justify-center min-h-screen p-4">
<Card className="w-full max-w-md relative">
<button
onClick={() => navigate('/')}
className="absolute top-4 left-4 text-gray-400 hover:text-white transition-colors z-10"
>
<ArrowLeft className="w-5 h-5" />
</button>
<CardHeader>
<CardTitle>Email Verification</CardTitle>
<CardDescription>
{verifying ? 'Verifying your email...' : 'Email verification status'}
</CardDescription>
</CardHeader>
<CardContent className="text-center space-y-4">
{verifying && (
<div className="flex flex-col items-center space-y-4">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
<p>Please wait while we verify your email...</p>
</div>
)}
{!verifying && verified && (
<div className="flex flex-col items-center space-y-4">
<CheckCircle className="h-12 w-12 text-green-500" />
<h3 className="text-lg font-semibold">Email Verified Successfully!</h3>
<p className="text-muted-foreground">
Your email has been verified. You can now access all features.
</p>
<Button onClick={() => navigate('/dashboard')}>
Go to Dashboard
</Button>
</div>
)}
{!verifying && !verified && (
<div className="flex flex-col items-center space-y-4">
<XCircle className="h-12 w-12 text-red-500" />
<h3 className="text-lg font-semibold">Verification Failed</h3>
<p className="text-muted-foreground">{error}</p>
<div className="flex gap-2">
<Button variant="outline" onClick={() => navigate('/dashboard')}>
Go to Dashboard
</Button>
<Button onClick={() => navigate('/login')}>
Login
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import AppLayout from '@/components/AppLayout';
import { AppProvider } from '@/contexts/AppContext';
const Index: React.FC = () => {
return (
<AppProvider>
<AppLayout />
</AppProvider>
);
};
export default Index;
+393
View File
@@ -0,0 +1,393 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Separator } from '@/components/ui/separator';
import { Checkbox } from '@/components/ui/checkbox';
import { Eye, EyeOff, Wallet, Mail, Lock, User, AlertCircle, ArrowLeft, UserPlus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
const Login: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { connectWallet, selectedAccount } = usePolkadot();
const { signIn, signUp } = useAuth();
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [loginData, setLoginData] = useState({
email: '',
password: ''
});
const [signupData, setSignupData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
referralCode: ''
});
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const { error } = await signIn(loginData.email, loginData.password);
if (error) {
if (error.message?.includes('Invalid login credentials')) {
setError('Email or password is incorrect. Please try again.');
} else {
setError(error.message || 'Login failed. Please try again.');
}
} else {
navigate('/');
}
} catch (err) {
setError('Login failed. Please try again.');
} finally {
setLoading(false);
}
};
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
if (signupData.password !== signupData.confirmPassword) {
setError('Passwords do not match');
setLoading(false);
return;
}
if (signupData.password.length < 8) {
setError('Password must be at least 8 characters');
setLoading(false);
return;
}
const { error } = await signUp(
signupData.email,
signupData.password,
signupData.name,
signupData.referralCode
);
if (error) {
setError(error.message);
} else {
navigate('/');
}
} catch (err) {
setError('Signup failed. Please try again.');
} finally {
setLoading(false);
}
};
const handleWalletConnect = async () => {
setLoading(true);
setError('');
try {
await connectWallet();
if (selectedAccount) {
navigate('/');
} else {
setError('Please select an account from your Polkadot.js extension');
}
} catch (err: any) {
console.error('Wallet connection failed:', err);
if (err.message?.includes('extension')) {
setError('Polkadot.js extension not found. Please install it first.');
} else {
setError('Failed to connect wallet. Please try again.');
}
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-[url('/grid.svg')] bg-center [mask-image:linear-gradient(180deg,white,rgba(255,255,255,0))]"></div>
<Card className="w-full max-w-md relative z-10 bg-gray-900/90 backdrop-blur-xl border-gray-800">
<CardHeader className="space-y-1">
<button
onClick={() => navigate('/')}
className="absolute top-4 left-4 text-gray-400 hover:text-white transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<CardTitle className="text-2xl font-bold text-center bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
PezkuwiChain
</CardTitle>
<CardDescription className="text-center text-gray-400">
{t('login.subtitle', 'Access your governance account')}
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="login" className="w-full">
<TabsList className="grid w-full grid-cols-2 bg-gray-800">
<TabsTrigger value="login">{t('login.signin', 'Sign In')}</TabsTrigger>
<TabsTrigger value="signup">{t('login.signup', 'Sign Up')}</TabsTrigger>
</TabsList>
<TabsContent value="login" className="space-y-4">
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email" className="text-gray-300">
{t('login.email', 'Email')}
</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="email"
type="email"
placeholder="name@example.com"
className="pl-10 bg-gray-800 border-gray-700 text-white"
value={loginData.email}
onChange={(e) => setLoginData({...loginData, email: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-gray-300">
{t('login.password', 'Password')}
</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="password"
type={showPassword ? 'text' : 'password'}
placeholder="••••••••"
className="pl-10 pr-10 bg-gray-800 border-gray-700 text-white"
value={loginData.password}
onChange={(e) => setLoginData({...loginData, password: e.target.value})}
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-gray-500 hover:text-gray-300"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
id="remember"
checked={rememberMe}
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
/>
<Label htmlFor="remember" className="text-sm text-gray-400 cursor-pointer">
{t('login.rememberMe', 'Remember me')}
</Label>
</div>
<button
type="button"
className="text-sm text-green-500 hover:text-green-400"
onClick={() => navigate('/reset-password')}
>
{t('login.forgotPassword', 'Forgot password?')}
</button>
</div>
{error && (
<Alert className="bg-red-900/20 border-red-800">
<AlertCircle className="h-4 w-4 text-red-500" />
<AlertDescription className="text-red-400">{error}</AlertDescription>
</Alert>
)}
<Button
type="submit"
className="w-full bg-gradient-to-r from-green-600 to-green-500 hover:from-green-500 hover:to-green-400"
disabled={loading}
>
{loading ? t('login.signingIn', 'Signing in...') : t('login.signin', 'Sign In')}
</Button>
</form>
</TabsContent>
<TabsContent value="signup" className="space-y-4">
<form onSubmit={handleSignup} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name" className="text-gray-300">
{t('login.fullName', 'Full Name')}
</Label>
<div className="relative">
<User className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="name"
type="text"
placeholder="John Doe"
className="pl-10 bg-gray-800 border-gray-700 text-white"
value={signupData.name}
onChange={(e) => setSignupData({...signupData, name: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="signup-email" className="text-gray-300">
{t('login.email', 'Email')}
</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="signup-email"
type="email"
placeholder="name@example.com"
className="pl-10 bg-gray-800 border-gray-700 text-white"
value={signupData.email}
onChange={(e) => setSignupData({...signupData, email: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="signup-password" className="text-gray-300">
{t('login.password', 'Password')}
</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="signup-password"
type={showPassword ? 'text' : 'password'}
placeholder="••••••••"
className="pl-10 pr-10 bg-gray-800 border-gray-700 text-white"
value={signupData.password}
onChange={(e) => setSignupData({...signupData, password: e.target.value})}
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-gray-500 hover:text-gray-300"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password" className="text-gray-300">
{t('login.confirmPassword', 'Confirm Password')}
</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="confirm-password"
type="password"
placeholder="••••••••"
className="pl-10 bg-gray-800 border-gray-700 text-white"
value={signupData.confirmPassword}
onChange={(e) => setSignupData({...signupData, confirmPassword: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="referral-code" className="text-gray-300">
{t('login.referralCode', 'Referral Code')}
<span className="text-gray-500 text-xs ml-1">({t('login.optional', 'Optional')})</span>
</Label>
<div className="relative">
<UserPlus className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="referral-code"
type="text"
placeholder={t('login.enterReferralCode', 'Enter referral code')}
className="pl-10 bg-gray-800 border-gray-700 text-white"
value={signupData.referralCode}
onChange={(e) => setSignupData({...signupData, referralCode: e.target.value})}
/>
</div>
<p className="text-xs text-gray-500">
{t('login.referralDescription', 'If someone referred you, enter their code here')}
</p>
</div>
{error && (
<Alert className="bg-red-900/20 border-red-800">
<AlertCircle className="h-4 w-4 text-red-500" />
<AlertDescription className="text-red-400">{error}</AlertDescription>
</Alert>
)}
<Button
type="submit"
className="w-full bg-gradient-to-r from-yellow-600 to-yellow-500 hover:from-yellow-500 hover:to-yellow-400"
disabled={loading}
>
{loading ? t('login.creatingAccount', 'Creating account...') : t('login.createAccount', 'Create Account')}
</Button>
</form>
</TabsContent>
</Tabs>
<div className="mt-6">
<Separator className="bg-gray-800" />
<div className="relative -top-3 text-center">
<span className="bg-gray-900 px-2 text-sm text-gray-500">
{t('login.or', 'Or continue with')}
</span>
</div>
</div>
<Button
variant="outline"
className="w-full border-gray-700 bg-gray-800 hover:bg-gray-700 text-white"
onClick={handleWalletConnect}
disabled={loading}
>
<Wallet className="mr-2 h-4 w-4" />
{t('login.connectWallet', 'Connect with Polkadot.js')}
</Button>
<p className="mt-2 text-xs text-center text-gray-500">
{t('login.walletHint', 'Connect your Polkadot wallet for instant access')}
</p>
</CardContent>
<CardFooter className="text-center text-sm text-gray-500">
<p>
{t('login.terms', 'By continuing, you agree to our')}{' '}
<a href="#" className="text-green-500 hover:text-green-400">
{t('login.termsOfService', 'Terms of Service')}
</a>{' '}
{t('login.and', 'and')}{' '}
<a href="#" className="text-green-500 hover:text-green-400">
{t('login.privacyPolicy', 'Privacy Policy')}
</a>
</p>
</CardFooter>
</Card>
</div>
);
};
export default Login;
+27
View File
@@ -0,0 +1,27 @@
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
const NotFound = () => {
const location = useLocation();
useEffect(() => {
console.error(
"404 Error: User attempted to access non-existent route:",
location.pathname
);
}, [location.pathname]);
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center p-8 rounded-lg border border-border bg-card shadow-md animate-slide-in">
<h1 className="text-5xl font-bold mb-6 text-primary">404</h1>
<p className="text-xl text-card-foreground mb-6">Page not found</p>
<a href="/" className="text-primary hover:text-primary/80 underline transition-colors">
Return to Home
</a>
</div>
</div>
);
};
export default NotFound;
+196
View File
@@ -0,0 +1,196 @@
import { useState } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { supabase } from '@/lib/supabase';
import { useToast } from '@/hooks/use-toast';
import { Loader2, ArrowLeft } from 'lucide-react';
export default function PasswordReset() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { toast } = useToast();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const token = searchParams.get('token');
const handleRequestReset = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const { data, error } = await supabase.functions.invoke('password-reset', {
body: { action: 'request', email }
});
if (error) throw error;
toast({
title: "Reset Email Sent",
description: "If the email exists, you'll receive a password reset link",
});
setEmail('');
} catch (error: any) {
toast({
title: "Error",
description: error.message || "Failed to send reset email",
variant: "destructive"
});
} finally {
setLoading(false);
}
};
const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== confirmPassword) {
toast({
title: "Error",
description: "Passwords do not match",
variant: "destructive"
});
return;
}
if (password.length < 8) {
toast({
title: "Error",
description: "Password must be at least 8 characters",
variant: "destructive"
});
return;
}
setLoading(true);
try {
const { data, error } = await supabase.functions.invoke('password-reset', {
body: { action: 'reset', token, newPassword: password }
});
if (error) throw error;
toast({
title: "Password Reset Successful",
description: "Your password has been updated",
});
navigate('/login');
} catch (error: any) {
toast({
title: "Error",
description: error.message || "Failed to reset password",
variant: "destructive"
});
} finally {
setLoading(false);
}
};
return (
<div className="container mx-auto flex items-center justify-center min-h-screen p-4">
<Card className="w-full max-w-md relative">
<button
onClick={() => navigate('/')}
className="absolute top-4 left-4 text-gray-400 hover:text-white transition-colors z-10"
>
<ArrowLeft className="w-5 h-5" />
</button>
<CardHeader>
<CardTitle>{token ? 'Reset Password' : 'Forgot Password'}</CardTitle>
<CardDescription>
{token
? 'Enter your new password below'
: 'Enter your email to receive a password reset link'}
</CardDescription>
</CardHeader>
<CardContent>
{!token ? (
<form onSubmit={handleRequestReset} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={loading}
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Send Reset Link
</Button>
<div className="text-center text-sm">
<Button
type="button"
variant="link"
onClick={() => navigate('/login')}
className="text-primary"
>
Back to Login
</Button>
</div>
</form>
) : (
<form onSubmit={handleResetPassword} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="password">New Password</Label>
<Input
id="password"
type="password"
placeholder="Enter new password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={loading}
minLength={8}
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<Input
id="confirmPassword"
type="password"
placeholder="Confirm new password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
disabled={loading}
minLength={8}
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Reset Password
</Button>
<div className="text-center text-sm">
<Button
type="button"
variant="link"
onClick={() => navigate('/login')}
className="text-primary"
>
Back to Login
</Button>
</div>
</form>
)}
</CardContent>
</Card>
</div>
);
}
+422
View File
@@ -0,0 +1,422 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { supabase } from '@/lib/supabase';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useToast } from '@/hooks/use-toast';
import { Loader2, User, Mail, Shield, Bell, Palette, Globe, ArrowLeft } from 'lucide-react';
import { TwoFactorSetup } from '@/components/auth/TwoFactorSetup';
export default function ProfileSettings() {
const navigate = useNavigate();
const { user } = useAuth();
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [profile, setProfile] = useState({
username: '',
full_name: '',
bio: '',
phone_number: '',
location: '',
website: '',
language: 'en',
theme: 'light',
notifications_email: true,
notifications_push: false,
notifications_sms: false,
two_factor_enabled: false
});
useEffect(() => {
if (user) {
loadProfile();
}
}, [user]);
const loadProfile = async () => {
try {
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user?.id)
.maybeSingle();
if (error) {
console.error('Error loading profile:', error);
return;
}
if (data) {
setProfile({
username: data.username || '',
full_name: data.full_name || '',
bio: data.bio || '',
phone_number: data.phone_number || '',
location: data.location || '',
website: data.website || '',
language: data.language || 'en',
theme: data.theme || 'light',
notifications_email: data.notifications_email ?? true,
notifications_push: data.notifications_push ?? false,
notifications_sms: data.notifications_sms ?? false,
two_factor_enabled: data.two_factor_enabled ?? false
});
}
} catch (error) {
console.error('Error loading profile:', error);
}
};
const updateProfile = async () => {
setLoading(true);
try {
// Call the secure upsert function
const { data, error } = await supabase.rpc('upsert_user_profile', {
p_username: profile.username || '',
p_full_name: profile.full_name || null,
p_bio: profile.bio || null,
p_phone_number: profile.phone_number || null,
p_location: profile.location || null,
p_website: profile.website || null,
p_language: profile.language || 'en',
p_theme: profile.theme || 'dark',
p_notifications_email: profile.notifications_email ?? true,
p_notifications_push: profile.notifications_push ?? false,
p_notifications_sms: profile.notifications_sms ?? false
});
if (error) throw error;
toast({
title: 'Success',
description: 'Profile updated successfully',
});
// Reload profile to ensure state is in sync
await loadProfile();
} catch (error: any) {
console.error('Profile update failed:', error);
toast({
title: 'Error',
description: error?.message || 'Failed to update profile',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const updateNotificationSettings = async () => {
setLoading(true);
try {
// Call the upsert function with current profile data + notification settings
const { data, error } = await supabase.rpc('upsert_user_profile', {
p_username: profile.username || '',
p_full_name: profile.full_name || null,
p_bio: profile.bio || null,
p_phone_number: profile.phone_number || null,
p_location: profile.location || null,
p_website: profile.website || null,
p_language: profile.language || 'en',
p_theme: profile.theme || 'dark',
p_notifications_email: profile.notifications_email ?? true,
p_notifications_push: profile.notifications_push ?? false,
p_notifications_sms: profile.notifications_sms ?? false
});
if (error) throw error;
toast({
title: 'Success',
description: 'Notification settings updated',
});
} catch (error: any) {
toast({
title: 'Error',
description: error?.message || 'Failed to update notification settings',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const updateSecuritySettings = async () => {
setLoading(true);
try {
const { error } = await supabase
.from('profiles')
.update({
two_factor_enabled: profile.two_factor_enabled,
updated_at: new Date().toISOString()
})
.eq('id', user?.id);
if (error) throw error;
toast({
title: 'Success',
description: 'Security settings updated',
});
} catch (error) {
toast({
title: 'Error',
description: 'Failed to update security settings',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const changePassword = async () => {
const newPassword = prompt('Enter new password:');
if (!newPassword) return;
setLoading(true);
try {
const { error } = await supabase.auth.updateUser({
password: newPassword
});
if (error) throw error;
toast({
title: 'Success',
description: 'Password changed successfully',
});
} catch (error) {
toast({
title: 'Error',
description: 'Failed to change password',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
return (
<div className="container mx-auto py-8 max-w-4xl relative">
<button
onClick={() => navigate('/')}
className="absolute top-4 left-4 text-gray-400 hover:text-white transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<h1 className="text-3xl font-bold mb-8">Profile Settings</h1>
<Tabs defaultValue="profile" className="space-y-4">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="profile">
<User className="mr-2 h-4 w-4" />
Profile
</TabsTrigger>
<TabsTrigger value="notifications">
<Bell className="mr-2 h-4 w-4" />
Notifications
</TabsTrigger>
<TabsTrigger value="security">
<Shield className="mr-2 h-4 w-4" />
Security
</TabsTrigger>
<TabsTrigger value="preferences">
<Palette className="mr-2 h-4 w-4" />
Preferences
</TabsTrigger>
</TabsList>
<TabsContent value="profile">
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label>Username</Label>
<Input
value={profile.username}
onChange={(e) => setProfile({ ...profile, username: e.target.value })}
placeholder="Enter username"
/>
</div>
<div>
<Label>Full Name</Label>
<Input
value={profile.full_name}
onChange={(e) => setProfile({ ...profile, full_name: e.target.value })}
placeholder="Enter full name"
/>
</div>
<div>
<Label>Bio</Label>
<Textarea
value={profile.bio}
onChange={(e) => setProfile({ ...profile, bio: e.target.value })}
placeholder="Tell us about yourself"
rows={4}
/>
</div>
<div>
<Label>Phone Number</Label>
<Input
value={profile.phone_number}
onChange={(e) => setProfile({ ...profile, phone_number: e.target.value })}
placeholder="+1234567890"
/>
</div>
<div>
<Label>Location</Label>
<Input
value={profile.location}
onChange={(e) => setProfile({ ...profile, location: e.target.value })}
placeholder="City, Country"
/>
</div>
<div>
<Label>Website</Label>
<Input
value={profile.website}
onChange={(e) => setProfile({ ...profile, website: e.target.value })}
placeholder="https://example.com"
/>
</div>
<Button onClick={updateProfile} disabled={loading}>
Save Changes
</Button>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="notifications">
<Card>
<CardHeader>
<CardTitle>Notification Preferences</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Email Notifications</Label>
<p className="text-sm text-muted-foreground">
Receive notifications via email
</p>
</div>
<Switch
checked={profile.notifications_email}
onCheckedChange={(checked) =>
setProfile({ ...profile, notifications_email: checked })
}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Push Notifications</Label>
<p className="text-sm text-muted-foreground">
Receive push notifications in browser
</p>
</div>
<Switch
checked={profile.notifications_push}
onCheckedChange={(checked) =>
setProfile({ ...profile, notifications_push: checked })
}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>SMS Notifications</Label>
<p className="text-sm text-muted-foreground">
Receive notifications via SMS
</p>
</div>
<Switch
checked={profile.notifications_sms}
onCheckedChange={(checked) =>
setProfile({ ...profile, notifications_sms: checked })
}
/>
</div>
<Button onClick={updateNotificationSettings} disabled={loading}>
Save Preferences
</Button>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="security">
<div className="space-y-4">
<TwoFactorSetup />
<Card>
<CardHeader>
<CardTitle>Password Security</CardTitle>
<CardDescription>
Manage your password settings
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Button variant="outline" onClick={changePassword}>
Change Password
</Button>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="preferences">
<Card>
<CardHeader>
<CardTitle>App Preferences</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label>Language</Label>
<Select
value={profile.language}
onValueChange={(value) => setProfile({ ...profile, language: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="tr">Türkçe</SelectItem>
<SelectItem value="ar">العربية</SelectItem>
<SelectItem value="kmr">Kurdî</SelectItem>
<SelectItem value="ckb">کوردی</SelectItem>
<SelectItem value="fa">فارسی</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Theme</Label>
<Select
value={profile.theme}
onValueChange={(value) => setProfile({ ...profile, theme: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</div>
<Button onClick={updateProfile} disabled={loading}>
Save Preferences
</Button>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}
+60
View File
@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ReservesDashboard } from '@/components/ReservesDashboard';
import { USDTBridge } from '@/components/USDTBridge';
// USDT Treasury Multisig Member Addresses
const SPECIFIC_ADDRESSES = {
// Non-unique roles - manually specified
Noter: '5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3',
Berdevk: '5F4V6dzpe72dE2C7YN3y7VGznMTWPFeSKL3ANhp4XasXjfvj',
};
const ReservesDashboardPage = () => {
const navigate = useNavigate();
const [isBridgeOpen, setIsBridgeOpen] = useState(false);
const [offChainReserve, setOffChainReserve] = useState(10000); // Example: $10,000 USDT
return (
<div className="min-h-screen bg-gray-950 pt-24 pb-12">
<div className="container mx-auto px-4 py-8 relative">
{/* Back Button */}
<button
onClick={() => navigate('/wallet')}
className="absolute top-4 left-4 text-gray-400 hover:text-white transition-colors flex items-center gap-2"
>
<ArrowLeft className="w-5 h-5" />
<span>Back to Wallet</span>
</button>
{/* Bridge Button */}
<div className="absolute top-4 right-4">
<Button
onClick={() => setIsBridgeOpen(true)}
className="bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700 flex items-center gap-2"
>
<Plus className="h-4 w-4" />
Deposit/Withdraw USDT
</Button>
</div>
{/* Main Content */}
<ReservesDashboard
specificAddresses={SPECIFIC_ADDRESSES}
offChainReserveAmount={offChainReserve}
/>
{/* Bridge Modal */}
<USDTBridge
isOpen={isBridgeOpen}
onClose={() => setIsBridgeOpen(false)}
specificAddresses={SPECIFIC_ADDRESSES}
/>
</div>
</div>
);
};
export default ReservesDashboardPage;
+390
View File
@@ -0,0 +1,390 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { AccountBalance } from '@/components/AccountBalance';
import { TransferModal } from '@/components/TransferModal';
import { ReceiveModal } from '@/components/ReceiveModal';
import { TransactionHistory } from '@/components/TransactionHistory';
import { NftList } from '@/components/NftList';
import { Button } from '@/components/ui/button';
import { ArrowUpRight, ArrowDownRight, History, ArrowLeft, RefreshCw } from 'lucide-react';
interface Transaction {
blockNumber: number;
extrinsicIndex: number;
hash: string;
method: string;
section: string;
from: string;
to?: string;
amount?: string;
success: boolean;
timestamp?: number;
}
const WalletDashboard: React.FC = () => {
const navigate = useNavigate();
const { api, isApiReady, selectedAccount } = usePolkadot();
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
const [isReceiveModalOpen, setIsReceiveModalOpen] = useState(false);
const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false);
const [recentTransactions, setRecentTransactions] = useState<Transaction[]>([]);
const [isLoadingRecent, setIsLoadingRecent] = useState(false);
// Fetch recent transactions
const fetchRecentTransactions = async () => {
if (!api || !isApiReady || !selectedAccount) return;
setIsLoadingRecent(true);
try {
const currentBlock = await api.rpc.chain.getBlock();
const currentBlockNumber = currentBlock.block.header.number.toNumber();
const txList: Transaction[] = [];
const blocksToCheck = Math.min(100, currentBlockNumber);
for (let i = 0; i < blocksToCheck && txList.length < 5; i++) {
const blockNumber = currentBlockNumber - i;
try {
const blockHash = await api.rpc.chain.getBlockHash(blockNumber);
const block = await api.rpc.chain.getBlock(blockHash);
let timestamp = 0;
try {
const ts = await api.query.timestamp.now.at(blockHash);
timestamp = ts.toNumber();
} catch (error) {
timestamp = Date.now();
}
block.block.extrinsics.forEach((extrinsic, index) => {
if (!extrinsic.isSigned) return;
const { method, signer } = extrinsic;
const fromAddress = signer.toString();
const isFromOurAccount = fromAddress === selectedAccount.address;
// Only track this account's transactions
if (!isFromOurAccount) return;
// Parse balances.transfer
if (method.section === 'balances' &&
(method.method === 'transfer' || method.method === 'transferKeepAlive')) {
const [dest, value] = method.args;
txList.push({
blockNumber,
extrinsicIndex: index,
hash: extrinsic.hash.toHex(),
method: method.method,
section: method.section,
from: fromAddress,
to: dest.toString(),
amount: value.toString(),
success: true,
timestamp: timestamp,
});
}
// Parse assets.transfer
else if (method.section === 'assets' && method.method === 'transfer') {
const [assetId, dest, value] = method.args;
txList.push({
blockNumber,
extrinsicIndex: index,
hash: extrinsic.hash.toHex(),
method: `${method.method} (Asset ${assetId.toString()})`,
section: method.section,
from: fromAddress,
to: dest.toString(),
amount: value.toString(),
success: true,
timestamp: timestamp,
});
}
// Parse staking operations
else if (method.section === 'staking') {
if (method.method === 'bond' || method.method === 'bondExtra') {
const value = method.args[method.method === 'bond' ? 1 : 0];
txList.push({
blockNumber,
extrinsicIndex: index,
hash: extrinsic.hash.toHex(),
method: method.method,
section: method.section,
from: fromAddress,
amount: value.toString(),
success: true,
timestamp: timestamp,
});
} else if (method.method === 'unbond') {
const [value] = method.args;
txList.push({
blockNumber,
extrinsicIndex: index,
hash: extrinsic.hash.toHex(),
method: method.method,
section: method.section,
from: fromAddress,
amount: value.toString(),
success: true,
timestamp: timestamp,
});
} else if (method.method === 'nominate' || method.method === 'withdrawUnbonded' || method.method === 'chill') {
txList.push({
blockNumber,
extrinsicIndex: index,
hash: extrinsic.hash.toHex(),
method: method.method,
section: method.section,
from: fromAddress,
success: true,
timestamp: timestamp,
});
}
}
// Parse DEX operations
else if (method.section === 'dex') {
if (method.method === 'swap') {
const [path, amountIn] = method.args;
txList.push({
blockNumber,
extrinsicIndex: index,
hash: extrinsic.hash.toHex(),
method: method.method,
section: method.section,
from: fromAddress,
amount: amountIn.toString(),
success: true,
timestamp: timestamp,
});
} else if (method.method === 'addLiquidity' || method.method === 'removeLiquidity') {
txList.push({
blockNumber,
extrinsicIndex: index,
hash: extrinsic.hash.toHex(),
method: method.method,
section: method.section,
from: fromAddress,
success: true,
timestamp: timestamp,
});
}
}
// Parse stakingScore & pezRewards
else if ((method.section === 'stakingScore' && method.method === 'startTracking') ||
(method.section === 'pezRewards' && method.method === 'claimReward')) {
txList.push({
blockNumber,
extrinsicIndex: index,
hash: extrinsic.hash.toHex(),
method: method.method,
section: method.section,
from: fromAddress,
success: true,
timestamp: timestamp,
});
}
});
} catch (blockError) {
// Continue to next block
}
}
setRecentTransactions(txList);
} catch (error) {
console.error('Failed to fetch recent transactions:', error);
} finally {
setIsLoadingRecent(false);
}
};
useEffect(() => {
if (selectedAccount && api && isApiReady) {
fetchRecentTransactions();
}
}, [selectedAccount, api, isApiReady]);
const formatAmount = (amount: string, decimals: number = 12) => {
const value = parseInt(amount) / Math.pow(10, decimals);
return value.toFixed(4);
};
const formatTimestamp = (timestamp?: number) => {
if (!timestamp) return 'Unknown';
const date = new Date(timestamp);
return date.toLocaleString();
};
const isIncoming = (tx: Transaction) => {
return tx.to === selectedAccount?.address;
};
if (!selectedAccount) {
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold text-white mb-4">Wallet Not Connected</h2>
<p className="text-gray-400 mb-6">Please connect your wallet to view your dashboard</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-950 pt-24 pb-12">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 relative">
<button
onClick={() => navigate('/')}
className="absolute top-4 left-4 text-gray-400 hover:text-white transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Wallet Dashboard</h1>
<p className="text-gray-400">Manage your HEZ and PEZ tokens</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Balance */}
<div className="lg:col-span-1">
<AccountBalance />
</div>
{/* Right Column - Actions */}
<div className="lg:col-span-2 space-y-6">
{/* Quick Actions */}
<div className="grid grid-cols-3 gap-4">
<Button
onClick={() => setIsTransferModalOpen(true)}
className="bg-gradient-to-r from-green-600 to-yellow-400 hover:from-green-700 hover:to-yellow-500 h-24 flex flex-col items-center justify-center"
>
<ArrowUpRight className="w-6 h-6 mb-2" />
<span>Send</span>
</Button>
<Button
onClick={() => setIsReceiveModalOpen(true)}
variant="outline"
className="border-gray-700 hover:bg-gray-800 h-24 flex flex-col items-center justify-center"
>
<ArrowDownRight className="w-6 h-6 mb-2" />
<span>Receive</span>
</Button>
<Button
onClick={() => setIsHistoryModalOpen(true)}
variant="outline"
className="border-gray-700 hover:bg-gray-800 h-24 flex flex-col items-center justify-center"
>
<History className="w-6 h-6 mb-2" />
<span>History</span>
</Button>
</div>
{/* Recent Activity */}
<div className="bg-gray-900 border border-gray-800 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white">Recent Activity</h3>
<Button
variant="ghost"
size="icon"
onClick={fetchRecentTransactions}
disabled={isLoadingRecent}
className="text-gray-400 hover:text-white"
>
<RefreshCw className={`w-4 h-4 ${isLoadingRecent ? 'animate-spin' : ''}`} />
</Button>
</div>
{isLoadingRecent ? (
<div className="text-center py-12">
<RefreshCw className="w-12 h-12 text-gray-600 mx-auto mb-3 animate-spin" />
<p className="text-gray-400">Loading transactions...</p>
</div>
) : recentTransactions.length === 0 ? (
<div className="text-center py-12">
<History className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-500">No recent transactions</p>
<p className="text-gray-600 text-sm mt-1">
Your transaction history will appear here
</p>
</div>
) : (
<div className="space-y-3">
{recentTransactions.map((tx, index) => (
<div
key={`${tx.blockNumber}-${tx.extrinsicIndex}`}
className="bg-gray-800/50 border border-gray-700 rounded-lg p-3 hover:bg-gray-800 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{isIncoming(tx) ? (
<div className="bg-green-500/20 p-2 rounded-lg">
<ArrowDownRight className="w-4 h-4 text-green-400" />
</div>
) : (
<div className="bg-yellow-500/20 p-2 rounded-lg">
<ArrowUpRight className="w-4 h-4 text-yellow-400" />
</div>
)}
<div>
<div className="text-white font-semibold text-sm">
{isIncoming(tx) ? 'Received' : 'Sent'}
</div>
<div className="text-xs text-gray-400">
Block #{tx.blockNumber}
</div>
</div>
</div>
<div className="text-right">
<div className="text-white font-mono text-sm">
{isIncoming(tx) ? '+' : '-'}{formatAmount(tx.amount || '0')}
</div>
<div className="text-xs text-gray-400">
{tx.section}.{tx.method}
</div>
</div>
</div>
</div>
))}
</div>
)}
<Button
onClick={() => setIsHistoryModalOpen(true)}
variant="outline"
className="mt-4 w-full border-gray-700 hover:bg-gray-800"
>
View All Transactions
</Button>
</div>
{/* NFT Collection */}
<NftList />
</div>
</div>
</div>
<TransferModal
isOpen={isTransferModalOpen}
onClose={() => setIsTransferModalOpen(false)}
/>
<ReceiveModal
isOpen={isReceiveModalOpen}
onClose={() => setIsReceiveModalOpen(false)}
/>
<TransactionHistory
isOpen={isHistoryModalOpen}
onClose={() => setIsHistoryModalOpen(false)}
/>
</div>
);
};
export default WalletDashboard;