mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-28 04:57:57 +00:00
Initial commit - PezkuwiChain Web Governance App
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
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 } 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 [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">
|
||||
<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,223 @@
|
||||
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 { supabase } from '@/lib/supabase';
|
||||
import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [profile, setProfile] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProfile();
|
||||
}, [user]);
|
||||
|
||||
const fetchProfile = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
setProfile(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching profile:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sendVerificationEmail = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase.functions.invoke('email-verification', {
|
||||
body: { action: 'send', email: user?.email }
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: "Verification Email Sent",
|
||||
description: "Please check your email for verification link",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to send verification email",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<h1 className="text-3xl font-bold mb-6">User Dashboard</h1>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3 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">
|
||||
{profile?.email_verified ? (
|
||||
<Badge className="bg-green-500">Verified</Badge>
|
||||
) : (
|
||||
<Badge variant="destructive">Unverified</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
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">
|
||||
{profile?.role || 'Member'}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Account type
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="profile" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="profile">Profile</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>
|
||||
{!profile?.email_verified && (
|
||||
<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 && (
|
||||
<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('/settings')}>Edit Profile</Button>
|
||||
</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>
|
||||
|
||||
{!profile?.email_verified && (
|
||||
<div className="border-l-4 border-yellow-500 bg-yellow-50 p-4">
|
||||
<div className="flex items-center">
|
||||
<AlertCircle className="h-5 w-5 text-yellow-600 mr-2" />
|
||||
<div>
|
||||
<h4 className="font-medium">Verify your email</h4>
|
||||
<p className="text-sm">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,90 @@
|
||||
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 } 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">
|
||||
<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,378 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
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 { TwoFactorVerify } from '@/components/auth/TwoFactorVerify';
|
||||
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 { useWallet } from '@/contexts/WalletContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { connect } = useWallet();
|
||||
const { signIn, signUp } = useAuth();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [requires2FA, setRequires2FA] = useState(false);
|
||||
const [tempUserId, setTempUserId] = useState('');
|
||||
|
||||
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) {
|
||||
// More user-friendly error messages
|
||||
if (error.message?.includes('Invalid login credentials')) {
|
||||
setError('Email veya şifre hatalı. Doğru bilgiler: info@pezkuwichain.io / Sq230515yBkB@#nm90');
|
||||
} else {
|
||||
setError(error.message || 'Giriş başarısız. Lütfen tekrar deneyin.');
|
||||
}
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Giriş yapılamadı. Lütfen tekrar deneyin.');
|
||||
} 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);
|
||||
try {
|
||||
await connect();
|
||||
navigate('/');
|
||||
} catch (err) {
|
||||
setError('Failed to connect wallet');
|
||||
} 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 Wallet')}
|
||||
</Button>
|
||||
</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,190 @@
|
||||
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 } 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">
|
||||
<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,378 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
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 } from 'lucide-react';
|
||||
import { TwoFactorSetup } from '@/components/auth/TwoFactorSetup';
|
||||
export default function ProfileSettings() {
|
||||
const { user } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [profile, setProfile] = useState({
|
||||
username: '',
|
||||
full_name: '',
|
||||
bio: '',
|
||||
phone: '',
|
||||
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 } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', user?.id)
|
||||
.single();
|
||||
|
||||
if (data) {
|
||||
setProfile({
|
||||
username: data.username || '',
|
||||
full_name: data.full_name || '',
|
||||
bio: data.bio || '',
|
||||
phone: data.phone || '',
|
||||
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 {
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
username: profile.username,
|
||||
full_name: profile.full_name,
|
||||
bio: profile.bio,
|
||||
phone: profile.phone,
|
||||
language: profile.language,
|
||||
theme: profile.theme,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', user?.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Profile updated successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update profile',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateNotificationSettings = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
notifications_email: profile.notifications_email,
|
||||
notifications_push: profile.notifications_push,
|
||||
notifications_sms: profile.notifications_sms,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', user?.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Notification settings updated',
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: '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">
|
||||
<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}
|
||||
onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
|
||||
placeholder="+1234567890"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user