445 lines
15 KiB
TypeScript
445 lines
15 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
|
import { ArrowLeft, Plus, MapPin, Edit, Trash2, Star } from 'lucide-react'
|
|
import { motion } from 'framer-motion'
|
|
import { toast } from 'sonner'
|
|
import Link from 'next/link'
|
|
|
|
interface Address {
|
|
id: string
|
|
firstName: string
|
|
lastName: string
|
|
company?: string
|
|
address1: string
|
|
address2?: string
|
|
city: string
|
|
state: string
|
|
zipCode: string
|
|
country: string
|
|
phone?: string
|
|
isDefault: boolean
|
|
type: 'HOME' | 'WORK' | 'OTHER'
|
|
}
|
|
|
|
export default function AddressesPage() {
|
|
const [addresses, setAddresses] = useState<Address[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
const [editingAddress, setEditingAddress] = useState<Address | null>(null)
|
|
const [formData, setFormData] = useState({
|
|
firstName: '',
|
|
lastName: '',
|
|
company: '',
|
|
address1: '',
|
|
address2: '',
|
|
city: '',
|
|
state: '',
|
|
zipCode: '',
|
|
country: 'India',
|
|
phone: '',
|
|
isDefault: false,
|
|
type: 'HOME' as 'HOME' | 'WORK' | 'OTHER'
|
|
})
|
|
|
|
useEffect(() => {
|
|
fetchAddresses()
|
|
}, [])
|
|
|
|
const fetchAddresses = async () => {
|
|
try {
|
|
const response = await fetch('/api/user/addresses')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setAddresses(data.addresses || [])
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching addresses:', error)
|
|
toast.error('Failed to load addresses')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
const requiredFields = ['firstName', 'lastName', 'address1', 'city', 'state', 'zipCode']
|
|
const missingFields = requiredFields.filter(field => !formData[field as keyof typeof formData])
|
|
|
|
if (missingFields.length > 0) {
|
|
toast.error('Please fill in all required fields')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const url = editingAddress ? `/api/user/addresses/${editingAddress.id}` : '/api/user/addresses'
|
|
const method = editingAddress ? 'PUT' : 'POST'
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(formData)
|
|
})
|
|
|
|
if (!response.ok) throw new Error('Failed to save address')
|
|
|
|
toast.success(editingAddress ? 'Address updated successfully' : 'Address added successfully')
|
|
setDialogOpen(false)
|
|
resetForm()
|
|
fetchAddresses()
|
|
} catch (error) {
|
|
toast.error('Failed to save address')
|
|
}
|
|
}
|
|
|
|
const handleEdit = (address: Address) => {
|
|
setEditingAddress(address)
|
|
setFormData({
|
|
firstName: address.firstName,
|
|
lastName: address.lastName,
|
|
company: address.company || '',
|
|
address1: address.address1,
|
|
address2: address.address2 || '',
|
|
city: address.city,
|
|
state: address.state,
|
|
zipCode: address.zipCode,
|
|
country: address.country,
|
|
phone: address.phone || '',
|
|
isDefault: address.isDefault,
|
|
type: address.type
|
|
})
|
|
setDialogOpen(true)
|
|
}
|
|
|
|
const handleDelete = async (addressId: string) => {
|
|
if (!confirm('Are you sure you want to delete this address?')) return
|
|
|
|
try {
|
|
const response = await fetch(`/api/user/addresses/${addressId}`, { method: 'DELETE' })
|
|
if (!response.ok) throw new Error('Failed to delete address')
|
|
|
|
toast.success('Address deleted successfully')
|
|
fetchAddresses()
|
|
} catch (error) {
|
|
toast.error('Failed to delete address')
|
|
}
|
|
}
|
|
|
|
const handleSetDefault = async (addressId: string) => {
|
|
try {
|
|
const response = await fetch(`/api/user/addresses/${addressId}/default`, { method: 'PUT' })
|
|
if (!response.ok) throw new Error('Failed to set default address')
|
|
|
|
toast.success('Default address updated')
|
|
fetchAddresses()
|
|
} catch (error) {
|
|
toast.error('Failed to update default address')
|
|
}
|
|
}
|
|
|
|
const resetForm = () => {
|
|
setFormData({
|
|
firstName: '',
|
|
lastName: '',
|
|
company: '',
|
|
address1: '',
|
|
address2: '',
|
|
city: '',
|
|
state: '',
|
|
zipCode: '',
|
|
country: 'India',
|
|
phone: '',
|
|
isDefault: false,
|
|
type: 'HOME' as 'HOME' | 'WORK' | 'OTHER'
|
|
})
|
|
setEditingAddress(null)
|
|
}
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const { name, value, type, checked } = e.target
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: type === 'checkbox' ? checked : value
|
|
}))
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
|
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white">
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<Button variant="ghost" asChild className="mb-4">
|
|
<Link href="/dashboard/profile">
|
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
Back to Profile
|
|
</Link>
|
|
</Button>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900">My Addresses</h1>
|
|
<p className="text-gray-600">Manage your shipping addresses</p>
|
|
</div>
|
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button onClick={resetForm}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Add Address
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{editingAddress ? 'Edit Address' : 'Add New Address'}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{editingAddress ? 'Update your address details' : 'Add a new shipping address'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="firstName">First Name *</Label>
|
|
<Input
|
|
id="firstName"
|
|
name="firstName"
|
|
value={formData.firstName}
|
|
onChange={handleInputChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="lastName">Last Name *</Label>
|
|
<Input
|
|
id="lastName"
|
|
name="lastName"
|
|
value={formData.lastName}
|
|
onChange={handleInputChange}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="company">Company (Optional)</Label>
|
|
<Input
|
|
id="company"
|
|
name="company"
|
|
value={formData.company}
|
|
onChange={handleInputChange}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="address1">Address Line 1 *</Label>
|
|
<Input
|
|
id="address1"
|
|
name="address1"
|
|
value={formData.address1}
|
|
onChange={handleInputChange}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="address2">Address Line 2 (Optional)</Label>
|
|
<Input
|
|
id="address2"
|
|
name="address2"
|
|
value={formData.address2}
|
|
onChange={handleInputChange}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="city">City *</Label>
|
|
<Input
|
|
id="city"
|
|
name="city"
|
|
value={formData.city}
|
|
onChange={handleInputChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="state">State *</Label>
|
|
<Input
|
|
id="state"
|
|
name="state"
|
|
value={formData.state}
|
|
onChange={handleInputChange}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="zipCode">ZIP Code *</Label>
|
|
<Input
|
|
id="zipCode"
|
|
name="zipCode"
|
|
value={formData.zipCode}
|
|
onChange={handleInputChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="phone">Phone</Label>
|
|
<Input
|
|
id="phone"
|
|
name="phone"
|
|
value={formData.phone}
|
|
onChange={handleInputChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="type">Address Type</Label>
|
|
<Select value={formData.type} onValueChange={(value) => setFormData(prev => ({ ...prev, type: value as 'HOME' | 'WORK' | 'OTHER' }))}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="HOME">Home</SelectItem>
|
|
<SelectItem value="WORK">Work</SelectItem>
|
|
<SelectItem value="OTHER">Other</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
id="isDefault"
|
|
name="isDefault"
|
|
checked={formData.isDefault}
|
|
onChange={handleInputChange}
|
|
className="rounded"
|
|
/>
|
|
<Label htmlFor="isDefault">Set as default address</Label>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit">
|
|
{editingAddress ? 'Update Address' : 'Add Address'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Addresses List */}
|
|
{addresses.length === 0 ? (
|
|
<Card>
|
|
<CardContent 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 addresses found</h3>
|
|
<p className="text-gray-500 mb-6">Add your first address to get started</p>
|
|
<Button onClick={() => setDialogOpen(true)}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Add Address
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{addresses.map((address, index) => (
|
|
<motion.div
|
|
key={address.id}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
>
|
|
<Card className="relative">
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<CardTitle className="text-lg">
|
|
{address.firstName} {address.lastName}
|
|
</CardTitle>
|
|
{address.isDefault && (
|
|
<Badge variant="default" className="bg-green-500">
|
|
<Star className="h-3 w-3 mr-1" />
|
|
Default
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<Badge variant="outline">{address.type}</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-2 text-sm text-gray-600">
|
|
{address.company && <p>{address.company}</p>}
|
|
<p>{address.address1}</p>
|
|
{address.address2 && <p>{address.address2}</p>}
|
|
<p>{address.city}, {address.state} {address.zipCode}</p>
|
|
<p>{address.country}</p>
|
|
{address.phone && <p>{address.phone}</p>}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between mt-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleEdit(address)}
|
|
>
|
|
<Edit className="h-3 w-3 mr-1" />
|
|
Edit
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleDelete(address.id)}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<Trash2 className="h-3 w-3 mr-1" />
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
|
|
{!address.isDefault && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleSetDefault(address.id)}
|
|
className="text-blue-600"
|
|
>
|
|
Set as Default
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|