mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-27 02:17:56 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user