From bf496f4a397917139d5a8f9c553f6318c1b7a1ea Mon Sep 17 00:00:00 2001 From: "ALMAZROUEI Shamma (2021) WKIS203" <shamma.almazrouei.2021@live.rhul.ac.uk> Date: Thu, 13 Mar 2025 00:44:56 +0530 Subject: [PATCH] Build profile page --- golden-crust-bakery/package-lock.json | 31 + golden-crust-bakery/package.json | 1 + .../src/components/ui/tabs.jsx | 41 ++ golden-crust-bakery/src/main.jsx | 5 + golden-crust-bakery/src/pages/Profile.jsx | 556 ++++++++++++++++++ 5 files changed, 634 insertions(+) create mode 100644 golden-crust-bakery/src/components/ui/tabs.jsx create mode 100644 golden-crust-bakery/src/pages/Profile.jsx diff --git a/golden-crust-bakery/package-lock.json b/golden-crust-bakery/package-lock.json index 4130bbf..30fcb51 100644 --- a/golden-crust-bakery/package-lock.json +++ b/golden-crust-bakery/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1810,6 +1811,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", diff --git a/golden-crust-bakery/package.json b/golden-crust-bakery/package.json index f016c50..bac8b20 100644 --- a/golden-crust-bakery/package.json +++ b/golden-crust-bakery/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/golden-crust-bakery/src/components/ui/tabs.jsx b/golden-crust-bakery/src/components/ui/tabs.jsx new file mode 100644 index 0000000..b674eb9 --- /dev/null +++ b/golden-crust-bakery/src/components/ui/tabs.jsx @@ -0,0 +1,41 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef(({ className, ...props }, ref) => ( + <TabsPrimitive.List + ref={ref} + className={cn( + "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", + className + )} + {...props} /> +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => ( + <TabsPrimitive.Trigger + ref={ref} + className={cn( + "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", + className + )} + {...props} /> +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef(({ className, ...props }, ref) => ( + <TabsPrimitive.Content + ref={ref} + className={cn( + "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + className + )} + {...props} /> +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/golden-crust-bakery/src/main.jsx b/golden-crust-bakery/src/main.jsx index 84ec698..7597294 100644 --- a/golden-crust-bakery/src/main.jsx +++ b/golden-crust-bakery/src/main.jsx @@ -20,6 +20,7 @@ import Cart from './pages/Cart'; import Orders from './pages/Orders'; import Checkout from './pages/Checkout'; import Confirmation from './pages/Confirmation'; +import Profile from './pages/Profile'; const router = createBrowserRouter([ { @@ -67,6 +68,10 @@ const router = createBrowserRouter([ path: '/menu', element: <Menu />, }, + { + path: '/profile', + element: <Profile />, + }, { path: '/menu/:id', element: <MenuItem />, diff --git a/golden-crust-bakery/src/pages/Profile.jsx b/golden-crust-bakery/src/pages/Profile.jsx new file mode 100644 index 0000000..7d712b3 --- /dev/null +++ b/golden-crust-bakery/src/pages/Profile.jsx @@ -0,0 +1,556 @@ +import { useState, useEffect } from 'react'; +import { toast } from 'sonner'; +import { useNavigate, Link } from 'react-router-dom'; +import { ArrowLeft, Key, Save, Trash2, User } from 'lucide-react'; + +import { useAuthStore } from '@/lib/authStore'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; + +export default function Profile() { + const navigate = useNavigate(); + const { user, isAuthenticated, updateUser, logout } = useAuthStore(); + + const [profileData, setProfileData] = useState({ + name: '', + email: '', + phone: '', + address: '', + city: '', + zipCode: '', + }); + + const [passwordData, setPasswordData] = useState({ + currentPassword: '', + newPassword: '', + confirmPassword: '', + }); + + const [errors, setErrors] = useState({}); + const [passwordErrors, setPasswordErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isPasswordSubmitting, setIsPasswordSubmitting] = useState(false); + + useEffect(() => { + // Redirect to login if not authenticated + if (!isAuthenticated) { + toast.error('Authentication Required', { + description: 'Please sign in to view your profile.', + }); + navigate('/login'); + return; + } + + // Load user data + if (user) { + // Get additional user data from localStorage if available + const users = JSON.parse(localStorage.getItem('users') || '[]'); + const userData = users.find((u) => u.id === user.id) || {}; + + setProfileData({ + name: user.name || '', + email: user.email || '', + phone: userData.phone || '', + address: userData.address || '', + city: userData.city || '', + zipCode: userData.zipCode || '', + }); + } + }, [isAuthenticated, navigate, user]); + + // If not authenticated, don't render the profile content + if (!isAuthenticated) { + return null; + } + + const handleProfileChange = (e) => { + const { name, value } = e.target; + setProfileData((prev) => ({ + ...prev, + [name]: value, + })); + + // Clear error when field is being edited + if (errors[name]) { + setErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + const handlePasswordChange = (e) => { + const { name, value } = e.target; + setPasswordData((prev) => ({ + ...prev, + [name]: value, + })); + + // Clear error when field is being edited + if (passwordErrors[name]) { + setPasswordErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + const validateProfileForm = () => { + const newErrors = {}; + + if (!profileData.name.trim()) newErrors.name = 'Name is required'; + if (!profileData.email.trim()) { + newErrors.email = 'Email is required'; + } else if (!/\S+@\S+\.\S+/.test(profileData.email)) { + newErrors.email = 'Email is invalid'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const validatePasswordForm = () => { + const newErrors = {}; + + if (!passwordData.currentPassword) { + newErrors.currentPassword = 'Current password is required'; + } + + if (!passwordData.newPassword) { + newErrors.newPassword = 'New password is required'; + } else if (passwordData.newPassword.length < 6) { + newErrors.newPassword = 'Password must be at least 6 characters'; + } + + if (passwordData.newPassword !== passwordData.confirmPassword) { + newErrors.confirmPassword = 'Passwords do not match'; + } + + setPasswordErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleProfileSubmit = (e) => { + e.preventDefault(); + + if (!validateProfileForm()) return; + + setIsSubmitting(true); + + // Simulate profile update + setTimeout(() => { + // Update user in localStorage + const users = JSON.parse(localStorage.getItem('users') || '[]'); + const updatedUsers = users.map((u) => { + if (u.id === user.id) { + return { + ...u, + name: profileData.name, + email: profileData.email, + phone: profileData.phone, + address: profileData.address, + city: profileData.city, + zipCode: profileData.zipCode, + }; + } + return u; + }); + + localStorage.setItem('users', JSON.stringify(updatedUsers)); + + // Update user in auth store + updateUser({ + name: profileData.name, + email: profileData.email, + }); + + toast.success('Profile Updated', { + description: 'Your profile information has been updated successfully.', + }); + + setIsSubmitting(false); + }, 1000); + }; + + const handlePasswordSubmit = (e) => { + e.preventDefault(); + + if (!validatePasswordForm()) return; + + setIsPasswordSubmitting(true); + + // Simulate password update + setTimeout(() => { + // Verify current password + const users = JSON.parse(localStorage.getItem('users') || '[]'); + const currentUser = users.find((u) => u.id === user.id); + + if ( + !currentUser || + currentUser.password !== passwordData.currentPassword + ) { + setPasswordErrors({ + currentPassword: 'Current password is incorrect', + }); + setIsPasswordSubmitting(false); + return; + } + + // Update password in localStorage + const updatedUsers = users.map((u) => { + if (u.id === user.id) { + return { + ...u, + password: passwordData.newPassword, + }; + } + return u; + }); + + localStorage.setItem('users', JSON.stringify(updatedUsers)); + + toast.success('Password Updated', { + description: 'Your password has been changed successfully.', + }); + + // Reset password fields + setPasswordData({ + currentPassword: '', + newPassword: '', + confirmPassword: '', + }); + + setIsPasswordSubmitting(false); + }, 1000); + }; + + const handleDeleteAccount = () => { + // Remove user from localStorage + const users = JSON.parse(localStorage.getItem('users') || '[]'); + const updatedUsers = users.filter((u) => u.id !== user.id); + localStorage.setItem('users', JSON.stringify(updatedUsers)); + + // Logout user + logout(); + + toast.success('Account Deleted', { + description: 'Your account has been deleted successfully.', + }); + + navigate('/'); + }; + + return ( + <div className='container mx-auto px-4 py-8 md:py-12'> + <Button + variant='ghost' + asChild + className='mb-6 text-gray-600 hover:text-gray-800' + > + <Link to='/'> + <ArrowLeft className='mr-2 h-4 w-4' /> Back to Home + </Link> + </Button> + + <div className='max-w-4xl mx-auto'> + <div className='text-center mb-8'> + <h1 className='text-3xl font-bold text-gray-800 mb-2'>My Profile</h1> + <p className='text-gray-600'> + Manage your account information and preferences + </p> + </div> + + <Tabs defaultValue='profile' className='space-y-6'> + <TabsList className='grid w-full grid-cols-2'> + <TabsTrigger value='profile'>Profile Information</TabsTrigger> + <TabsTrigger value='security'>Security</TabsTrigger> + </TabsList> + + <TabsContent value='profile'> + <Card> + <CardHeader> + <CardTitle>Profile Information</CardTitle> + <CardDescription> + Update your personal information and delivery details + </CardDescription> + </CardHeader> + <form onSubmit={handleProfileSubmit}> + <CardContent className='space-y-6'> + <div className='space-y-4'> + <div className='grid grid-cols-1 md:grid-cols-2 gap-6'> + <div className='space-y-2'> + <Label htmlFor='name'>Full Name</Label> + <div className='relative'> + <User className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5' /> + <Input + id='name' + name='name' + value={profileData.name} + onChange={handleProfileChange} + className={`pl-10 rounded-lg ${ + errors.name ? 'border-red-500' : 'border-gray-200' + }`} + /> + </div> + {errors.name && ( + <p className='text-red-500 text-sm'>{errors.name}</p> + )} + </div> + + <div className='space-y-2'> + <Label htmlFor='email'>Email Address</Label> + <Input + id='email' + name='email' + type='email' + value={profileData.email} + onChange={handleProfileChange} + className={`rounded-lg ${ + errors.email ? 'border-red-500' : 'border-gray-200' + }`} + /> + {errors.email && ( + <p className='text-red-500 text-sm'>{errors.email}</p> + )} + </div> + </div> + + <div className='space-y-2'> + <Label htmlFor='phone'>Phone Number</Label> + <Input + id='phone' + name='phone' + value={profileData.phone} + onChange={handleProfileChange} + className='rounded-lg border-gray-200' + /> + </div> + + <div className='space-y-2'> + <Label htmlFor='address'>Address</Label> + <Input + id='address' + name='address' + value={profileData.address} + onChange={handleProfileChange} + className='rounded-lg border-gray-200' + /> + </div> + + <div className='grid grid-cols-1 md:grid-cols-2 gap-6'> + <div className='space-y-2'> + <Label htmlFor='city'>City</Label> + <Input + id='city' + name='city' + value={profileData.city} + onChange={handleProfileChange} + className='rounded-lg border-gray-200' + /> + </div> + + <div className='space-y-2'> + <Label htmlFor='zipCode'>ZIP Code</Label> + <Input + id='zipCode' + name='zipCode' + value={profileData.zipCode} + onChange={handleProfileChange} + className='rounded-lg border-gray-200' + /> + </div> + </div> + </div> + </CardContent> + <CardFooter className='flex justify-end'> + <Button + type='submit' + disabled={isSubmitting} + className='bg-pink-500 hover:bg-pink-600 text-white rounded-full' + > + {isSubmitting ? ( + 'Saving...' + ) : ( + <> + <Save className='mr-2 h-4 w-4' /> Save Changes + </> + )} + </Button> + </CardFooter> + </form> + </Card> + </TabsContent> + + <TabsContent value='security'> + <div className='space-y-6'> + <Card> + <CardHeader> + <CardTitle>Change Password</CardTitle> + <CardDescription> + Update your password to keep your account secure + </CardDescription> + </CardHeader> + <form onSubmit={handlePasswordSubmit}> + <CardContent className='space-y-4'> + <div className='space-y-2'> + <Label htmlFor='currentPassword'>Current Password</Label> + <div className='relative'> + <Key className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5' /> + <Input + id='currentPassword' + name='currentPassword' + type='password' + value={passwordData.currentPassword} + onChange={handlePasswordChange} + className={`pl-10 rounded-lg ${ + passwordErrors.currentPassword + ? 'border-red-500' + : 'border-gray-200' + }`} + /> + </div> + {passwordErrors.currentPassword && ( + <p className='text-red-500 text-sm'> + {passwordErrors.currentPassword} + </p> + )} + </div> + + <div className='space-y-2'> + <Label htmlFor='newPassword'>New Password</Label> + <div className='relative'> + <Key className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5' /> + <Input + id='newPassword' + name='newPassword' + type='password' + value={passwordData.newPassword} + onChange={handlePasswordChange} + className={`pl-10 rounded-lg ${ + passwordErrors.newPassword + ? 'border-red-500' + : 'border-gray-200' + }`} + /> + </div> + {passwordErrors.newPassword && ( + <p className='text-red-500 text-sm'> + {passwordErrors.newPassword} + </p> + )} + </div> + + <div className='space-y-2'> + <Label htmlFor='confirmPassword'> + Confirm New Password + </Label> + <div className='relative'> + <Key className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5' /> + <Input + id='confirmPassword' + name='confirmPassword' + type='password' + value={passwordData.confirmPassword} + onChange={handlePasswordChange} + className={`pl-10 rounded-lg ${ + passwordErrors.confirmPassword + ? 'border-red-500' + : 'border-gray-200' + }`} + /> + </div> + {passwordErrors.confirmPassword && ( + <p className='text-red-500 text-sm'> + {passwordErrors.confirmPassword} + </p> + )} + </div> + </CardContent> + <CardFooter className='flex justify-end'> + <Button + type='submit' + disabled={isPasswordSubmitting} + className='bg-pink-500 hover:bg-pink-600 text-white rounded-full' + > + {isPasswordSubmitting ? 'Updating...' : 'Update Password'} + </Button> + </CardFooter> + </form> + </Card> + + <Card className='border-red-100'> + <CardHeader> + <CardTitle className='text-red-600'>Delete Account</CardTitle> + <CardDescription> + Permanently delete your account and all associated data + </CardDescription> + </CardHeader> + <CardContent> + <p className='text-gray-600'> + This action cannot be undone. Once you delete your account, + all your data will be permanently removed from our system. + </p> + </CardContent> + <CardFooter> + <AlertDialog> + <AlertDialogTrigger asChild> + <Button + variant='outline' + className='border-red-300 text-red-600 hover:bg-red-50 rounded-full' + > + <Trash2 className='mr-2 h-4 w-4' /> Delete Account + </Button> + </AlertDialogTrigger> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle> + Are you absolutely sure? + </AlertDialogTitle> + <AlertDialogDescription> + This action cannot be undone. This will permanently + delete your account and remove all your data from our + servers. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>Cancel</AlertDialogCancel> + <AlertDialogAction + onClick={handleDeleteAccount} + className='bg-red-500 hover:bg-red-600 text-white' + > + Yes, Delete My Account + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </CardFooter> + </Card> + </div> + </TabsContent> + </Tabs> + </div> + </div> + ); +} -- GitLab