Spaces:
Running
Running
import { useState } from "react"; | |
import { useLocation } from "wouter"; | |
import { useAuth } from "@/hooks/use-auth"; | |
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; | |
import { userApi, ordersApi } from "@/lib/api"; | |
import Header from "@/components/layout/header"; | |
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
import { Button } from "@/components/ui/button"; | |
import { Badge } from "@/components/ui/badge"; | |
import { Separator } from "@/components/ui/separator"; | |
import { Skeleton } from "@/components/ui/skeleton"; | |
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
import { Input } from "@/components/ui/input"; | |
import { Label } from "@/components/ui/label"; | |
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; | |
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; | |
import { Switch } from "@/components/ui/switch"; | |
import { useToast } from "@/hooks/use-toast"; | |
import { User, Package, MapPin, CreditCard, Settings, LogOut, Edit, Save, X, Plus, Trash2, Mail, Bell, Shield, Download, Eye, EyeOff } from "lucide-react"; | |
export default function Profile() { | |
const [, setLocation] = useLocation(); | |
const { user, logout, isLoading } = useAuth(); | |
const [searchQuery, setSearchQuery] = useState(""); | |
const [isEditing, setIsEditing] = useState(false); | |
const [editForm, setEditForm] = useState<any>({}); | |
const [activeTab, setActiveTab] = useState("profile"); | |
const [addresses, setAddresses] = useState<any[]>([]); | |
const [paymentMethods, setPaymentMethods] = useState<any[]>([]); | |
const [addressForm, setAddressForm] = useState<any>({}); | |
const [paymentForm, setPaymentForm] = useState<any>({}); | |
const [isAddingAddress, setIsAddingAddress] = useState(false); | |
const [isAddingPayment, setIsAddingPayment] = useState(false); | |
const [settings, setSettings] = useState({ | |
emailNotifications: true, | |
orderNotifications: true, | |
promotionalEmails: false, | |
twoFactorAuth: false, | |
showProfile: true | |
}); | |
const [showPassword, setShowPassword] = useState(false); | |
const [passwordForm, setPasswordForm] = useState({ current: '', new: '', confirm: '' }); | |
const { toast } = useToast(); | |
const queryClient = useQueryClient(); | |
// Always call hooks before any early returns | |
const { data: profile, isLoading: profileLoading } = useQuery({ | |
queryKey: ['/api/user/profile'], | |
queryFn: async () => { | |
const response = await userApi.getProfile(); | |
return response.json(); | |
}, | |
enabled: !!user, // Only fetch when user is authenticated | |
}); | |
const { data: orders = [], isLoading: ordersLoading } = useQuery({ | |
queryKey: ['/api/orders'], | |
queryFn: async () => { | |
const response = await ordersApi.get(); | |
return response.json(); | |
}, | |
enabled: !!user, // Only fetch when user is authenticated | |
}); | |
const updateProfileMutation = useMutation({ | |
mutationFn: (data: any) => userApi.updateProfile(data), | |
onSuccess: () => { | |
queryClient.invalidateQueries({ queryKey: ['/api/user/profile'] }); | |
setIsEditing(false); | |
toast({ | |
title: "Profile updated", | |
description: "Your profile has been successfully updated.", | |
}); | |
}, | |
onError: () => { | |
toast({ | |
title: "Error", | |
description: "Failed to update profile. Please try again.", | |
variant: "destructive", | |
}); | |
}, | |
}); | |
// Show loading while authentication is being verified | |
if (isLoading) { | |
return ( | |
<div className="min-h-screen bg-background flex items-center justify-center"> | |
<div className="text-center"> | |
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div> | |
<p className="text-muted-foreground">Loading...</p> | |
</div> | |
</div> | |
); | |
} | |
// Redirect if not authenticated after loading completes | |
if (!user) { | |
setLocation('/auth'); | |
return null; | |
} | |
const handleEditProfile = () => { | |
setEditForm({ | |
firstName: profile?.firstName || user.firstName || '', | |
lastName: profile?.lastName || user.lastName || '', | |
email: profile?.email || user.email || '', | |
phone: profile?.phone || '', | |
street: profile?.street || '', | |
city: profile?.city || '', | |
state: profile?.state || '', | |
pinCode: profile?.pinCode || '', | |
country: profile?.country || '', | |
}); | |
setIsEditing(true); | |
}; | |
const handleSaveProfile = () => { | |
updateProfileMutation.mutate(editForm); | |
}; | |
const handleCancelEdit = () => { | |
setIsEditing(false); | |
setEditForm({}); | |
}; | |
const handleInputChange = (field: string, value: string) => { | |
setEditForm(prev => ({ ...prev, [field]: value })); | |
}; | |
const handleTabNavigation = (section: string) => { | |
setActiveTab(section); | |
if (section === 'orders') { | |
setLocation('/orders'); | |
} | |
}; | |
const handleLogout = () => { | |
logout(); | |
setLocation('/'); | |
}; | |
// Address functions | |
const handleAddAddress = () => { | |
setAddressForm({ type: 'home', street: '', city: '', state: '', pinCode: '', country: '', isDefault: false }); | |
setIsAddingAddress(true); | |
}; | |
const handleSaveAddress = () => { | |
if (!addressForm.street || !addressForm.city || !addressForm.state || !addressForm.pinCode) { | |
toast({ title: "Error", description: "Please fill in all required fields.", variant: "destructive" }); | |
return; | |
} | |
const newAddress = { ...addressForm, id: Date.now().toString() }; | |
setAddresses(prev => [...prev, newAddress]); | |
setIsAddingAddress(false); | |
setAddressForm({}); | |
toast({ title: "Success", description: "Address added successfully." }); | |
}; | |
const handleDeleteAddress = (addressId: string) => { | |
setAddresses(prev => prev.filter(addr => addr.id !== addressId)); | |
toast({ title: "Success", description: "Address deleted successfully." }); | |
}; | |
// Payment functions | |
const handleAddPayment = () => { | |
setPaymentForm({ type: 'card', cardNumber: '', expiryDate: '', cvv: '', nameOnCard: '', isDefault: false }); | |
setIsAddingPayment(true); | |
}; | |
const handleSavePayment = () => { | |
if (!paymentForm.cardNumber || !paymentForm.expiryDate || !paymentForm.cvv || !paymentForm.nameOnCard) { | |
toast({ title: "Error", description: "Please fill in all required fields.", variant: "destructive" }); | |
return; | |
} | |
const newPayment = { | |
...paymentForm, | |
id: Date.now().toString(), | |
cardNumber: `****-****-****-${paymentForm.cardNumber.slice(-4)}` | |
}; | |
setPaymentMethods(prev => [...prev, newPayment]); | |
setIsAddingPayment(false); | |
setPaymentForm({}); | |
toast({ title: "Success", description: "Payment method added successfully." }); | |
}; | |
const handleDeletePayment = (paymentId: string) => { | |
setPaymentMethods(prev => prev.filter(payment => payment.id !== paymentId)); | |
toast({ title: "Success", description: "Payment method deleted successfully." }); | |
}; | |
// Settings functions | |
const handleSettingsChange = (key: string, value: boolean) => { | |
setSettings(prev => ({ ...prev, [key]: value })); | |
toast({ title: "Settings updated", description: "Your preferences have been saved." }); | |
}; | |
const handlePasswordChange = () => { | |
if (!passwordForm.current || !passwordForm.new || !passwordForm.confirm) { | |
toast({ title: "Error", description: "Please fill in all password fields.", variant: "destructive" }); | |
return; | |
} | |
if (passwordForm.new !== passwordForm.confirm) { | |
toast({ title: "Error", description: "New passwords do not match.", variant: "destructive" }); | |
return; | |
} | |
if (passwordForm.new.length < 6) { | |
toast({ title: "Error", description: "Password must be at least 6 characters long.", variant: "destructive" }); | |
return; | |
} | |
setPasswordForm({ current: '', new: '', confirm: '' }); | |
toast({ title: "Success", description: "Password changed successfully." }); | |
}; | |
const handleDataExport = () => { | |
toast({ title: "Data Export", description: "Your data export will be emailed to you within 24 hours." }); | |
}; | |
const handleAccountDelete = () => { | |
toast({ title: "Account Deletion", description: "Please contact support to delete your account.", variant: "destructive" }); | |
}; | |
const getStatusColor = (status: string) => { | |
switch (status.toLowerCase()) { | |
case 'delivered': | |
return 'default'; | |
case 'shipped': | |
return 'secondary'; | |
case 'pending': | |
return 'outline'; | |
case 'cancelled': | |
return 'destructive'; | |
default: | |
return 'outline'; | |
} | |
}; | |
return ( | |
<div className="min-h-screen bg-gray-50"> | |
<Header searchQuery={searchQuery} setSearchQuery={setSearchQuery} /> | |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" data-testid="profile-page"> | |
<div className="mb-8"> | |
<h1 className="text-3xl font-bold text-gray-900" data-testid="profile-title"> | |
My Account | |
</h1> | |
<p className="text-gray-600 mt-2">Manage your account and view your order history</p> | |
</div> | |
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8"> | |
{/* Account Navigation */} | |
<div className="lg:col-span-1"> | |
<Card> | |
<CardContent className="p-4"> | |
<nav className="space-y-2" data-testid="profile-nav"> | |
<Button | |
variant="ghost" | |
className={`w-full justify-start ${ | |
activeTab === 'profile' ? 'bg-primary/10 text-primary' : '' | |
}`} | |
onClick={() => handleTabNavigation('profile')} | |
> | |
<User className="mr-2 h-4 w-4" /> | |
Profile | |
</Button> | |
<Button | |
variant="ghost" | |
className={`w-full justify-start ${ | |
activeTab === 'orders' ? 'bg-primary/10 text-primary' : '' | |
}`} | |
onClick={() => handleTabNavigation('orders')} | |
> | |
<Package className="mr-2 h-4 w-4" /> | |
Order History | |
</Button> | |
<Button | |
variant="ghost" | |
className={`w-full justify-start ${ | |
activeTab === 'addresses' ? 'bg-primary/10 text-primary' : '' | |
}`} | |
onClick={() => handleTabNavigation('addresses')} | |
> | |
<MapPin className="mr-2 h-4 w-4" /> | |
Addresses | |
</Button> | |
<Button | |
variant="ghost" | |
className={`w-full justify-start ${ | |
activeTab === 'payment' ? 'bg-primary/10 text-primary' : '' | |
}`} | |
onClick={() => handleTabNavigation('payment')} | |
> | |
<CreditCard className="mr-2 h-4 w-4" /> | |
Payment Methods | |
</Button> | |
<Button | |
variant="ghost" | |
className={`w-full justify-start ${ | |
activeTab === 'settings' ? 'bg-primary/10 text-primary' : '' | |
}`} | |
onClick={() => handleTabNavigation('settings')} | |
> | |
<Settings className="mr-2 h-4 w-4" /> | |
Settings | |
</Button> | |
<Separator /> | |
<Button | |
variant="ghost" | |
className="w-full justify-start text-red-600 hover:text-red-700 hover:bg-red-50" | |
onClick={handleLogout} | |
data-testid="logout-button" | |
> | |
<LogOut className="mr-2 h-4 w-4" /> | |
Sign Out | |
</Button> | |
</nav> | |
</CardContent> | |
</Card> | |
</div> | |
{/* Account Content */} | |
<div className="lg:col-span-3"> | |
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6"> | |
<TabsList className="hidden"> | |
<TabsTrigger value="profile" data-testid="profile-tab">Profile Information</TabsTrigger> | |
<TabsTrigger value="orders" data-testid="orders-tab">Order History</TabsTrigger> | |
<TabsTrigger value="addresses" data-testid="addresses-tab">Addresses</TabsTrigger> | |
<TabsTrigger value="payment" data-testid="payment-tab">Payment Methods</TabsTrigger> | |
<TabsTrigger value="settings" data-testid="settings-tab">Settings</TabsTrigger> | |
</TabsList> | |
<TabsContent value="profile"> | |
<Card> | |
<CardHeader> | |
<CardTitle>Profile Information</CardTitle> | |
</CardHeader> | |
<CardContent> | |
{profileLoading ? ( | |
<div className="space-y-4"> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
<Skeleton className="h-10 w-full" /> | |
<Skeleton className="h-10 w-full" /> | |
</div> | |
<Skeleton className="h-10 w-full" /> | |
<Skeleton className="h-10 w-full" /> | |
</div> | |
) : ( | |
<div className="space-y-6" data-testid="profile-info"> | |
{!isEditing ? ( | |
<> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
<div> | |
<Label>First Name</Label> | |
<div className="p-3 bg-gray-50 rounded-lg" data-testid="first-name"> | |
{profile?.firstName || user.firstName || 'Not provided'} | |
</div> | |
</div> | |
<div> | |
<Label>Last Name</Label> | |
<div className="p-3 bg-gray-50 rounded-lg" data-testid="last-name"> | |
{profile?.lastName || user.lastName || 'Not provided'} | |
</div> | |
</div> | |
</div> | |
<div> | |
<Label>Email</Label> | |
<div className="p-3 bg-gray-50 rounded-lg" data-testid="email"> | |
{profile?.email || user.email} | |
</div> | |
</div> | |
<div> | |
<Label>Username</Label> | |
<div className="p-3 bg-gray-50 rounded-lg" data-testid="username"> | |
{profile?.username || user.username} | |
</div> | |
</div> | |
<div> | |
<Label>Phone</Label> | |
<div className="p-3 bg-gray-50 rounded-lg" data-testid="phone"> | |
{profile?.phone || 'Not provided'} | |
</div> | |
</div> | |
<div> | |
<Label>Address</Label> | |
<div className="p-3 bg-gray-50 rounded-lg" data-testid="address"> | |
{(profile?.street || profile?.city) ? | |
`${profile.street || ''}, ${profile.city || ''}, ${profile.state || ''} ${profile.pinCode || ''}, ${profile.country || ''}`.replace(/^,\s*|,\s*$|,\s*,/g, '') : | |
'Not provided' | |
} | |
</div> | |
</div> | |
<Button | |
onClick={handleEditProfile} | |
className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600" | |
data-testid="edit-profile" | |
> | |
<Edit className="h-4 w-4 mr-2" /> | |
Edit Profile | |
</Button> | |
</> | |
) : ( | |
<> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
<div> | |
<Label htmlFor="firstName">First Name</Label> | |
<Input | |
id="firstName" | |
value={editForm.firstName || ''} | |
onChange={(e) => handleInputChange('firstName', e.target.value)} | |
data-testid="edit-first-name" | |
/> | |
</div> | |
<div> | |
<Label htmlFor="lastName">Last Name</Label> | |
<Input | |
id="lastName" | |
value={editForm.lastName || ''} | |
onChange={(e) => handleInputChange('lastName', e.target.value)} | |
data-testid="edit-last-name" | |
/> | |
</div> | |
</div> | |
<div> | |
<Label htmlFor="email">Email</Label> | |
<Input | |
id="email" | |
type="email" | |
value={editForm.email || ''} | |
onChange={(e) => handleInputChange('email', e.target.value)} | |
data-testid="edit-email" | |
/> | |
</div> | |
<div> | |
<Label htmlFor="phone">Phone</Label> | |
<Input | |
id="phone" | |
value={editForm.phone || ''} | |
onChange={(e) => handleInputChange('phone', e.target.value)} | |
data-testid="edit-phone" | |
/> | |
</div> | |
<div className="space-y-4"> | |
<h4 className="font-medium text-gray-900">Address Information</h4> | |
<div> | |
<Label htmlFor="street">Street Address</Label> | |
<Input | |
id="street" | |
value={editForm.street || ''} | |
onChange={(e) => handleInputChange('street', e.target.value)} | |
data-testid="edit-street" | |
/> | |
</div> | |
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
<div> | |
<Label htmlFor="city">City</Label> | |
<Input | |
id="city" | |
value={editForm.city || ''} | |
onChange={(e) => handleInputChange('city', e.target.value)} | |
data-testid="edit-city" | |
/> | |
</div> | |
<div> | |
<Label htmlFor="state">State</Label> | |
<Input | |
id="state" | |
value={editForm.state || ''} | |
onChange={(e) => handleInputChange('state', e.target.value)} | |
data-testid="edit-state" | |
/> | |
</div> | |
<div> | |
<Label htmlFor="pinCode">PIN Code</Label> | |
<Input | |
id="pinCode" | |
value={editForm.pinCode || ''} | |
onChange={(e) => handleInputChange('pinCode', e.target.value)} | |
data-testid="edit-pin-code" | |
/> | |
</div> | |
</div> | |
<div> | |
<Label htmlFor="country">Country</Label> | |
<Input | |
id="country" | |
value={editForm.country || ''} | |
onChange={(e) => handleInputChange('country', e.target.value)} | |
data-testid="edit-country" | |
/> | |
</div> | |
</div> | |
<div className="flex space-x-3"> | |
<Button | |
onClick={handleSaveProfile} | |
disabled={updateProfileMutation.isPending} | |
className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600" | |
data-testid="save-profile" | |
> | |
<Save className="h-4 w-4 mr-2" /> | |
{updateProfileMutation.isPending ? 'Saving...' : 'Save Changes'} | |
</Button> | |
<Button | |
variant="outline" | |
onClick={handleCancelEdit} | |
data-testid="cancel-edit" | |
> | |
<X className="h-4 w-4 mr-2" /> | |
Cancel | |
</Button> | |
</div> | |
</> | |
)} | |
</div> | |
)} | |
</CardContent> | |
</Card> | |
</TabsContent> | |
<TabsContent value="addresses"> | |
<Card> | |
<CardHeader> | |
<div className="flex items-center justify-between"> | |
<CardTitle className="flex items-center"> | |
<MapPin className="h-5 w-5 mr-2" /> | |
Addresses | |
</CardTitle> | |
<Dialog open={isAddingAddress} onOpenChange={setIsAddingAddress}> | |
<DialogTrigger asChild> | |
<Button onClick={handleAddAddress} data-testid="add-address"> | |
<Plus className="h-4 w-4 mr-2" /> | |
Add Address | |
</Button> | |
</DialogTrigger> | |
<DialogContent> | |
<DialogHeader> | |
<DialogTitle>Add New Address</DialogTitle> | |
</DialogHeader> | |
<div className="space-y-4"> | |
<div> | |
<Label htmlFor="addressType">Address Type</Label> | |
<Select value={addressForm.type || ''} onValueChange={(value) => setAddressForm(prev => ({ ...prev, type: value }))}> | |
<SelectTrigger> | |
<SelectValue placeholder="Select address type" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="home">Home</SelectItem> | |
<SelectItem value="work">Work</SelectItem> | |
<SelectItem value="other">Other</SelectItem> | |
</SelectContent> | |
</Select> | |
</div> | |
<div> | |
<Label htmlFor="street">Street Address</Label> | |
<Input | |
id="street" | |
value={addressForm.street || ''} | |
onChange={(e) => setAddressForm(prev => ({ ...prev, street: e.target.value }))} | |
placeholder="Enter street address" | |
/> | |
</div> | |
<div className="grid grid-cols-2 gap-4"> | |
<div> | |
<Label htmlFor="city">City</Label> | |
<Input | |
id="city" | |
value={addressForm.city || ''} | |
onChange={(e) => setAddressForm(prev => ({ ...prev, city: e.target.value }))} | |
placeholder="Enter city" | |
/> | |
</div> | |
<div> | |
<Label htmlFor="state">State</Label> | |
<Input | |
id="state" | |
value={addressForm.state || ''} | |
onChange={(e) => setAddressForm(prev => ({ ...prev, state: e.target.value }))} | |
placeholder="Enter state" | |
/> | |
</div> | |
</div> | |
<div className="grid grid-cols-2 gap-4"> | |
<div> | |
<Label htmlFor="pinCode">PIN Code</Label> | |
<Input | |
id="pinCode" | |
value={addressForm.pinCode || ''} | |
onChange={(e) => setAddressForm(prev => ({ ...prev, pinCode: e.target.value }))} | |
placeholder="Enter PIN code" | |
/> | |
</div> | |
<div> | |
<Label htmlFor="country">Country</Label> | |
<Input | |
id="country" | |
value={addressForm.country || ''} | |
onChange={(e) => setAddressForm(prev => ({ ...prev, country: e.target.value }))} | |
placeholder="Enter country" | |
/> | |
</div> | |
</div> | |
<div className="flex items-center space-x-2"> | |
<Switch | |
id="defaultAddress" | |
checked={addressForm.isDefault || false} | |
onCheckedChange={(checked) => setAddressForm(prev => ({ ...prev, isDefault: checked }))} | |
/> | |
<Label htmlFor="defaultAddress">Set as default address</Label> | |
</div> | |
<div className="flex space-x-2"> | |
<Button onClick={handleSaveAddress} className="flex-1">Save Address</Button> | |
<Button variant="outline" onClick={() => setIsAddingAddress(false)} className="flex-1">Cancel</Button> | |
</div> | |
</div> | |
</DialogContent> | |
</Dialog> | |
</div> | |
</CardHeader> | |
<CardContent> | |
{addresses.length === 0 ? ( | |
<div className="text-center py-12"> | |
<MapPin className="h-16 w-16 text-gray-400 mx-auto mb-4" /> | |
<h3 className="text-lg font-medium text-gray-900 mb-2">No saved addresses</h3> | |
<p className="text-gray-500 mb-6">Add addresses for faster checkout</p> | |
</div> | |
) : ( | |
<div className="space-y-4" data-testid="addresses-list"> | |
{addresses.map((address: any) => ( | |
<div key={address.id} className="border rounded-lg p-4" data-testid={`address-${address.id}`}> | |
<div className="flex justify-between items-start"> | |
<div className="flex-1"> | |
<div className="flex items-center space-x-2 mb-2"> | |
<Badge variant={address.isDefault ? 'default' : 'secondary'}> | |
{address.type.charAt(0).toUpperCase() + address.type.slice(1)} | |
</Badge> | |
{address.isDefault && <Badge>Default</Badge>} | |
</div> | |
<p className="text-gray-900">{address.street}</p> | |
<p className="text-gray-600">{address.city}, {address.state} {address.pinCode}</p> | |
<p className="text-gray-600">{address.country}</p> | |
</div> | |
<Button | |
variant="outline" | |
size="sm" | |
onClick={() => handleDeleteAddress(address.id)} | |
className="text-red-600 hover:text-red-700" | |
data-testid={`delete-address-${address.id}`} | |
> | |
<Trash2 className="h-4 w-4" /> | |
</Button> | |
</div> | |
</div> | |
))} | |
</div> | |
)} | |
</CardContent> | |
</Card> | |
</TabsContent> | |
<TabsContent value="payment"> | |
<Card> | |
<CardHeader> | |
<div className="flex items-center justify-between"> | |
<CardTitle className="flex items-center"> | |
<CreditCard className="h-5 w-5 mr-2" /> | |
Payment Methods | |
</CardTitle> | |
<Dialog open={isAddingPayment} onOpenChange={setIsAddingPayment}> | |
<DialogTrigger asChild> | |
<Button onClick={handleAddPayment} data-testid="add-payment"> | |
<Plus className="h-4 w-4 mr-2" /> | |
Add Payment Method | |
</Button> | |
</DialogTrigger> | |
<DialogContent> | |
<DialogHeader> | |
<DialogTitle>Add Payment Method</DialogTitle> | |
</DialogHeader> | |
<div className="space-y-4"> | |
<div> | |
<Label htmlFor="nameOnCard">Name on Card</Label> | |
<Input | |
id="nameOnCard" | |
value={paymentForm.nameOnCard || ''} | |
onChange={(e) => setPaymentForm(prev => ({ ...prev, nameOnCard: e.target.value }))} | |
placeholder="Enter name on card" | |
/> | |
</div> | |
<div> | |
<Label htmlFor="cardNumber">Card Number</Label> | |
<Input | |
id="cardNumber" | |
value={paymentForm.cardNumber || ''} | |
onChange={(e) => setPaymentForm(prev => ({ ...prev, cardNumber: e.target.value }))} | |
placeholder="1234 5678 9012 3456" | |
maxLength={19} | |
/> | |
</div> | |
<div className="grid grid-cols-2 gap-4"> | |
<div> | |
<Label htmlFor="expiryDate">Expiry Date</Label> | |
<Input | |
id="expiryDate" | |
value={paymentForm.expiryDate || ''} | |
onChange={(e) => setPaymentForm(prev => ({ ...prev, expiryDate: e.target.value }))} | |
placeholder="MM/YY" | |
maxLength={5} | |
/> | |
</div> | |
<div> | |
<Label htmlFor="cvv">CVV</Label> | |
<Input | |
id="cvv" | |
value={paymentForm.cvv || ''} | |
onChange={(e) => setPaymentForm(prev => ({ ...prev, cvv: e.target.value }))} | |
placeholder="123" | |
maxLength={4} | |
/> | |
</div> | |
</div> | |
<div className="flex items-center space-x-2"> | |
<Switch | |
id="defaultPayment" | |
checked={paymentForm.isDefault || false} | |
onCheckedChange={(checked) => setPaymentForm(prev => ({ ...prev, isDefault: checked }))} | |
/> | |
<Label htmlFor="defaultPayment">Set as default payment method</Label> | |
</div> | |
<div className="flex space-x-2"> | |
<Button onClick={handleSavePayment} className="flex-1">Save Payment Method</Button> | |
<Button variant="outline" onClick={() => setIsAddingPayment(false)} className="flex-1">Cancel</Button> | |
</div> | |
</div> | |
</DialogContent> | |
</Dialog> | |
</div> | |
</CardHeader> | |
<CardContent> | |
{paymentMethods.length === 0 ? ( | |
<div className="text-center py-12"> | |
<CreditCard className="h-16 w-16 text-gray-400 mx-auto mb-4" /> | |
<h3 className="text-lg font-medium text-gray-900 mb-2">No saved payment methods</h3> | |
<p className="text-gray-500 mb-6">Add payment methods for faster checkout</p> | |
</div> | |
) : ( | |
<div className="space-y-4" data-testid="payment-methods-list"> | |
{paymentMethods.map((payment: any) => ( | |
<div key={payment.id} className="border rounded-lg p-4" data-testid={`payment-${payment.id}`}> | |
<div className="flex justify-between items-start"> | |
<div className="flex-1"> | |
<div className="flex items-center space-x-2 mb-2"> | |
<Badge variant={payment.isDefault ? 'default' : 'secondary'}> | |
Credit Card | |
</Badge> | |
{payment.isDefault && <Badge>Default</Badge>} | |
</div> | |
<p className="text-gray-900 font-medium">{payment.nameOnCard}</p> | |
<p className="text-gray-600">{payment.cardNumber}</p> | |
<p className="text-gray-600">Expires: {payment.expiryDate}</p> | |
</div> | |
<Button | |
variant="outline" | |
size="sm" | |
onClick={() => handleDeletePayment(payment.id)} | |
className="text-red-600 hover:text-red-700" | |
data-testid={`delete-payment-${payment.id}`} | |
> | |
<Trash2 className="h-4 w-4" /> | |
</Button> | |
</div> | |
</div> | |
))} | |
</div> | |
)} | |
</CardContent> | |
</Card> | |
</TabsContent> | |
<TabsContent value="settings"> | |
<div className="space-y-6"> | |
{/* Notifications */} | |
<Card> | |
<CardHeader> | |
<CardTitle className="flex items-center"> | |
<Bell className="h-5 w-5 mr-2" /> | |
Notifications | |
</CardTitle> | |
</CardHeader> | |
<CardContent className="space-y-4"> | |
<div className="flex items-center justify-between"> | |
<div> | |
<Label>Email Notifications</Label> | |
<p className="text-sm text-gray-500">Receive order updates and promotions via email</p> | |
</div> | |
<Switch | |
checked={settings.emailNotifications} | |
onCheckedChange={(checked) => handleSettingsChange('emailNotifications', checked)} | |
data-testid="email-notifications" | |
/> | |
</div> | |
<div className="flex items-center justify-between"> | |
<div> | |
<Label>Order Notifications</Label> | |
<p className="text-sm text-gray-500">Get notified about order status changes</p> | |
</div> | |
<Switch | |
checked={settings.orderNotifications} | |
onCheckedChange={(checked) => handleSettingsChange('orderNotifications', checked)} | |
data-testid="order-notifications" | |
/> | |
</div> | |
<div className="flex items-center justify-between"> | |
<div> | |
<Label>Promotional Emails</Label> | |
<p className="text-sm text-gray-500">Receive offers and deals via email</p> | |
</div> | |
<Switch | |
checked={settings.promotionalEmails} | |
onCheckedChange={(checked) => handleSettingsChange('promotionalEmails', checked)} | |
data-testid="promotional-emails" | |
/> | |
</div> | |
</CardContent> | |
</Card> | |
{/* Security */} | |
<Card> | |
<CardHeader> | |
<CardTitle className="flex items-center"> | |
<Shield className="h-5 w-5 mr-2" /> | |
Security | |
</CardTitle> | |
</CardHeader> | |
<CardContent className="space-y-6"> | |
<div className="flex items-center justify-between"> | |
<div> | |
<Label>Two-Factor Authentication</Label> | |
<p className="text-sm text-gray-500">Add an extra layer of security to your account</p> | |
</div> | |
<Switch | |
checked={settings.twoFactorAuth} | |
onCheckedChange={(checked) => handleSettingsChange('twoFactorAuth', checked)} | |
data-testid="two-factor-auth" | |
/> | |
</div> | |
<div className="border-t pt-4"> | |
<h4 className="font-medium text-gray-900 mb-3">Change Password</h4> | |
<div className="space-y-3"> | |
<div> | |
<Label>Current Password</Label> | |
<div className="relative"> | |
<Input | |
type={showPassword ? 'text' : 'password'} | |
value={passwordForm.current} | |
onChange={(e) => setPasswordForm(prev => ({ ...prev, current: e.target.value }))} | |
placeholder="Enter current password" | |
data-testid="current-password" | |
/> | |
<Button | |
type="button" | |
variant="ghost" | |
size="sm" | |
className="absolute right-0 top-0 h-full px-3" | |
onClick={() => setShowPassword(!showPassword)} | |
> | |
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} | |
</Button> | |
</div> | |
</div> | |
<div> | |
<Label>New Password</Label> | |
<Input | |
type="password" | |
value={passwordForm.new} | |
onChange={(e) => setPasswordForm(prev => ({ ...prev, new: e.target.value }))} | |
placeholder="Enter new password" | |
data-testid="new-password" | |
/> | |
</div> | |
<div> | |
<Label>Confirm New Password</Label> | |
<Input | |
type="password" | |
value={passwordForm.confirm} | |
onChange={(e) => setPasswordForm(prev => ({ ...prev, confirm: e.target.value }))} | |
placeholder="Confirm new password" | |
data-testid="confirm-password" | |
/> | |
</div> | |
<Button onClick={handlePasswordChange} data-testid="change-password">Change Password</Button> | |
</div> | |
</div> | |
</CardContent> | |
</Card> | |
{/* Privacy */} | |
<Card> | |
<CardHeader> | |
<CardTitle className="flex items-center"> | |
<Eye className="h-5 w-5 mr-2" /> | |
Privacy | |
</CardTitle> | |
</CardHeader> | |
<CardContent className="space-y-4"> | |
<div className="flex items-center justify-between"> | |
<div> | |
<Label>Show Profile Publicly</Label> | |
<p className="text-sm text-gray-500">Make your profile visible to other users</p> | |
</div> | |
<Switch | |
checked={settings.showProfile} | |
onCheckedChange={(checked) => handleSettingsChange('showProfile', checked)} | |
data-testid="show-profile" | |
/> | |
</div> | |
</CardContent> | |
</Card> | |
{/* Data & Account */} | |
<Card> | |
<CardHeader> | |
<CardTitle className="flex items-center"> | |
<Download className="h-5 w-5 mr-2" /> | |
Data & Account | |
</CardTitle> | |
</CardHeader> | |
<CardContent className="space-y-4"> | |
<div className="flex items-center justify-between"> | |
<div> | |
<Label>Export Data</Label> | |
<p className="text-sm text-gray-500">Download all your account data</p> | |
</div> | |
<Button variant="outline" onClick={handleDataExport} data-testid="export-data"> | |
<Download className="h-4 w-4 mr-2" /> | |
Export | |
</Button> | |
</div> | |
<div className="flex items-center justify-between"> | |
<div> | |
<Label>Delete Account</Label> | |
<p className="text-sm text-gray-500">Permanently delete your account and all data</p> | |
</div> | |
<Button | |
variant="outline" | |
onClick={handleAccountDelete} | |
className="text-red-600 hover:text-red-700 border-red-200 hover:border-red-300" | |
data-testid="delete-account" | |
> | |
<Trash2 className="h-4 w-4 mr-2" /> | |
Delete | |
</Button> | |
</div> | |
</CardContent> | |
</Card> | |
</div> | |
</TabsContent> | |
<TabsContent value="orders"> | |
<Card> | |
<CardHeader> | |
<CardTitle>Order History</CardTitle> | |
</CardHeader> | |
<CardContent> | |
{ordersLoading ? ( | |
<div className="space-y-4"> | |
{[...Array(3)].map((_, i) => ( | |
<div key={i} className="border rounded-lg p-4"> | |
<Skeleton className="h-6 w-48 mb-2" /> | |
<Skeleton className="h-4 w-32 mb-4" /> | |
<div className="space-y-2"> | |
<Skeleton className="h-12 w-full" /> | |
<Skeleton className="h-12 w-full" /> | |
</div> | |
</div> | |
))} | |
</div> | |
) : orders.length > 0 ? ( | |
<div className="space-y-4" data-testid="orders-list"> | |
{orders.map((order: any) => ( | |
<div key={order.id} className="border border-gray-200 rounded-lg p-4" data-testid={`order-${order.id}`}> | |
<div className="flex justify-between items-start mb-3"> | |
<div> | |
<h4 className="font-medium text-gray-900" data-testid={`order-id-${order.id}`}> | |
Order #{order.id.slice(0, 8)} | |
</h4> | |
<p className="text-sm text-gray-500" data-testid={`order-date-${order.id}`}> | |
Placed on {new Date(order.createdAt).toLocaleDateString()} | |
</p> | |
</div> | |
<Badge variant={getStatusColor(order.status)} data-testid={`order-status-${order.id}`}> | |
{order.status} | |
</Badge> | |
</div> | |
{order.items && order.items.length > 0 && ( | |
<div className="space-y-2 mb-4"> | |
{order.items.map((item: any) => ( | |
<div key={item.id} className="flex items-center space-x-3" data-testid={`order-item-${item.id}`}> | |
<div className="w-12 h-12 bg-gray-100 rounded overflow-hidden"> | |
{item.product?.images && item.product.images.length > 0 ? ( | |
<img | |
src={item.product.images[0]} | |
alt={item.product.title} | |
className="w-full h-full object-cover" | |
/> | |
) : ( | |
<div className="w-full h-full bg-gray-200 flex items-center justify-center"> | |
<span className="text-xs text-gray-400">No Image</span> | |
</div> | |
)} | |
</div> | |
<div className="flex-1"> | |
<p className="text-sm font-medium text-gray-900" data-testid={`order-item-title-${item.id}`}> | |
{item.product?.title || 'Unknown Product'} | |
</p> | |
<p className="text-xs text-gray-500" data-testid={`order-item-seller-${item.id}`}> | |
{item.product?.store?.name || 'Unknown Seller'} | |
</p> | |
</div> | |
<div className="text-right"> | |
<p className="text-sm font-semibold text-gray-900" data-testid={`order-item-price-${item.id}`}> | |
₹{parseFloat(item.price).toFixed(2)} | |
</p> | |
<p className="text-xs text-gray-500" data-testid={`order-item-quantity-${item.id}`}> | |
Qty: {item.quantity} | |
</p> | |
</div> | |
</div> | |
))} | |
</div> | |
)} | |
<div className="flex justify-between items-center pt-3 border-t border-gray-200"> | |
<span className="font-semibold text-gray-900" data-testid={`order-total-${order.id}`}> | |
Total: ₹{parseFloat(order.total).toFixed(2)} | |
</span> | |
<div className="space-x-2"> | |
<Button variant="outline" size="sm" data-testid={`track-order-${order.id}`}> | |
Track Order | |
</Button> | |
<Button variant="outline" size="sm" data-testid={`reorder-${order.id}`}> | |
Reorder | |
</Button> | |
</div> | |
</div> | |
</div> | |
))} | |
</div> | |
) : ( | |
<div className="text-center py-12" data-testid="no-orders"> | |
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" /> | |
<p className="text-gray-500">You haven't placed any orders yet.</p> | |
<Button | |
className="mt-4" | |
onClick={() => setLocation('/')} | |
data-testid="start-shopping" | |
> | |
Start Shopping | |
</Button> | |
</div> | |
)} | |
</CardContent> | |
</Card> | |
</TabsContent> | |
</Tabs> | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
} | |