mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 06:47:55 +00:00
9073fa54f3
Root cause: Token symbol mismatch between TokenSwap component and WalletContext - WalletContext stored balance with key 'USDT' - TokenSwap used symbol 'wUSDT' for lookups - This caused balances['wUSDT'] to return undefined - Triggered false "insufficient balance" errors Changes: - Updated TokenSwap.tsx to use 'USDT' symbol consistently - Fixed token symbol in AVAILABLE_TOKENS array - Updated asset ID mapping in swap transaction logic - Fixed swap history token display Also includes: - Added new DEX components (SwapInterface, PoolBrowser, etc.) - Added TypeScript type definitions for DEX - Added DEX utility functions - Removed obsolete test scripts - Updated WalletContext with USDT balance support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
422 lines
14 KiB
TypeScript
422 lines
14 KiB
TypeScript
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>
|
||
);
|
||
} |