From c9f6c9b97b76c0358dc629cf8e73414bd536be58 Mon Sep 17 00:00:00 2001 From: "ALMAZROUEI Shamma (2021) WKIS203" <shamma.almazrouei.2021@live.rhul.ac.uk> Date: Wed, 12 Mar 2025 17:43:08 +0530 Subject: [PATCH] Build orders page --- golden-crust-bakery/package-lock.json | 29 ++ golden-crust-bakery/package.json | 1 + .../src/components/ui/alert-dialog.jsx | 97 ++++++ golden-crust-bakery/src/main.jsx | 5 + golden-crust-bakery/src/pages/Orders.jsx | 276 ++++++++++++++++++ 5 files changed, 408 insertions(+) create mode 100644 golden-crust-bakery/src/components/ui/alert-dialog.jsx create mode 100644 golden-crust-bakery/src/pages/Orders.jsx diff --git a/golden-crust-bakery/package-lock.json b/golden-crust-bakery/package-lock.json index 9d36fc4..87b1ca4 100644 --- a/golden-crust-bakery/package-lock.json +++ b/golden-crust-bakery/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@radix-ui/react-accordion": "^1.2.3", + "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -1185,6 +1186,34 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", + "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "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-arrow": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", diff --git a/golden-crust-bakery/package.json b/golden-crust-bakery/package.json index d63182e..45050f1 100644 --- a/golden-crust-bakery/package.json +++ b/golden-crust-bakery/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@radix-ui/react-accordion": "^1.2.3", + "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", diff --git a/golden-crust-bakery/src/components/ui/alert-dialog.jsx b/golden-crust-bakery/src/components/ui/alert-dialog.jsx new file mode 100644 index 0000000..a4174f3 --- /dev/null +++ b/golden-crust-bakery/src/components/ui/alert-dialog.jsx @@ -0,0 +1,97 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Overlay + className={cn( + "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + className + )} + {...props} + ref={ref} /> +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => ( + <AlertDialogPortal> + <AlertDialogOverlay /> + <AlertDialogPrimitive.Content + ref={ref} + className={cn( + "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", + className + )} + {...props} /> + </AlertDialogPortal> +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}) => ( + <div + className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} + {...props} /> +) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}) => ( + <div + className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} + {...props} /> +) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} /> +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Description + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} /> +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} /> +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Cancel + ref={ref} + className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)} + {...props} /> +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/golden-crust-bakery/src/main.jsx b/golden-crust-bakery/src/main.jsx index f7c36cc..aa26fb2 100644 --- a/golden-crust-bakery/src/main.jsx +++ b/golden-crust-bakery/src/main.jsx @@ -17,6 +17,7 @@ import Menu from './pages/Menu'; import MenuItem from './pages/MenuItem'; import ErrorPage from './pages/Error'; import Cart from './pages/Cart'; +import Orders from './pages/Orders'; const router = createBrowserRouter([ { @@ -48,6 +49,10 @@ const router = createBrowserRouter([ path: '/cart', element: <Cart />, }, + { + path: '/orders', + element: <Orders />, + }, { path: '/menu', element: <Menu />, diff --git a/golden-crust-bakery/src/pages/Orders.jsx b/golden-crust-bakery/src/pages/Orders.jsx new file mode 100644 index 0000000..17d5886 --- /dev/null +++ b/golden-crust-bakery/src/pages/Orders.jsx @@ -0,0 +1,276 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import { Home, Package, ShoppingBag, X } from 'lucide-react'; + +import { useOrderStore } from '@/lib/orderStore'; +import { useAuthStore } from '@/lib/authStore'; + +import { Button } from '@/components/ui/button'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; + +export default function Orders() { + const navigate = useNavigate(); + const { orders, cancelOrder, initializeOrders } = useOrderStore(); + const { isAuthenticated } = useAuthStore(); + + useEffect(() => { + initializeOrders(); + + // Redirect to login if not authenticated + if (!isAuthenticated) { + toast({ + title: 'Authentication Required', + description: 'Please sign in to view your orders.', + variant: 'destructive', + }); + navigate('/login'); + } + }, [initializeOrders, isAuthenticated, navigate]); + + // If not authenticated, don't render the orders content + if (!isAuthenticated) { + return null; + } + + const handleCancelOrder = (orderId) => { + cancelOrder(orderId); + toast({ + title: 'Order Cancelled', + description: 'Your order has been cancelled successfully.', + }); + }; + + const getStatusColor = (status) => { + switch (status) { + case 'processing': + return 'bg-yellow-100 text-yellow-800'; + case 'shipped': + return 'bg-blue-100 text-blue-800'; + case 'delivered': + return 'bg-green-100 text-green-800'; + case 'cancelled': + return 'bg-red-100 text-red-800'; + default: + return 'bg-gray-100 text-gray-800'; + } + }; + + return ( + <div className='container mx-auto px-4 py-8 md:py-12'> + <div className='text-center mb-8'> + <h1 className='text-4xl font-bold text-gray-800 mb-4'>My Orders</h1> + <p className='text-gray-600'> + {orders.length > 0 + ? `You have ${orders.length} order${orders.length !== 1 ? 's' : ''}` + : 'You have no orders yet'} + </p> + </div> + + {orders.length > 0 ? ( + <div className='max-w-4xl mx-auto'> + <Accordion type='single' collapsible className='space-y-4'> + {orders.map((order) => ( + <AccordionItem + key={order.id} + value={order.id} + className='bg-white rounded-xl shadow-md overflow-hidden border-none' + > + <AccordionTrigger className='px-6 py-4 hover:no-underline hover:bg-gray-50'> + <div className='flex flex-col md:flex-row w-full items-start md:items-center justify-between text-left'> + <div> + <p className='font-semibold text-gray-900'> + Order #{order.id.split('-')[1]} + </p> + <p className='text-sm text-gray-500'> + {new Date(order.date).toLocaleDateString()} + </p> + </div> + <div className='flex items-center mt-2 md:mt-0'> + <div + className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor( + order.status + )} mr-4`} + > + {order.status.charAt(0).toUpperCase() + + order.status.slice(1)} + </div> + <p className='font-medium text-pink-600'> + ${order.total.toFixed(2)} + </p> + </div> + </div> + </AccordionTrigger> + <AccordionContent className='px-6 pb-6'> + <div className='border-t border-gray-200 pt-4'> + <div className='space-y-4'> + <div> + <h3 className='text-sm font-medium text-gray-800 mb-2'> + Items Ordered: + </h3> + <ul className='divide-y divide-gray-200'> + {order.items.map((item) => ( + <li + key={item.id} + className='py-3 flex justify-between' + > + <div> + <p className='text-sm font-medium text-gray-900'> + {item.name} + </p> + <p className='text-sm text-gray-500'> + Qty: {item.quantity} + </p> + </div> + <p className='text-sm font-medium text-gray-900'> + ${(item.price * item.quantity).toFixed(2)} + </p> + </li> + ))} + </ul> + </div> + + <div className='grid grid-cols-1 md:grid-cols-2 gap-6'> + <div> + <h3 className='text-sm font-medium text-gray-800 mb-2'> + Delivery Information: + </h3> + <p className='text-sm text-gray-600'> + {order.customer.name} + </p> + <p className='text-sm text-gray-600'> + {order.customer.address} + </p> + <p className='text-sm text-gray-600'> + {order.customer.city}, {order.customer.zipCode} + </p> + </div> + + <div> + <h3 className='text-sm font-medium text-gray-800 mb-2'> + Order Summary: + </h3> + <div className='space-y-1'> + <div className='flex justify-between'> + <p className='text-sm text-gray-600'>Subtotal</p> + <p className='text-sm text-gray-900'> + ${order.subtotal.toFixed(2)} + </p> + </div> + <div className='flex justify-between'> + <p className='text-sm text-gray-600'>Tax</p> + <p className='text-sm text-gray-900'> + ${order.tax.toFixed(2)} + </p> + </div> + <div className='flex justify-between'> + <p className='text-sm text-gray-600'> + Delivery Fee + </p> + <p className='text-sm text-gray-900'> + ${order.deliveryFee.toFixed(2)} + </p> + </div> + <div className='flex justify-between border-t border-gray-200 pt-1 mt-1'> + <p className='text-sm font-medium text-gray-900'> + Total + </p> + <p className='text-sm font-medium text-pink-600'> + ${order.total.toFixed(2)} + </p> + </div> + </div> + </div> + </div> + + <div className='flex justify-end mt-4'> + {order.status !== 'cancelled' && ( + <AlertDialog> + <AlertDialogTrigger asChild> + <Button + variant='outline' + className='border-red-300 text-red-600 hover:bg-red-50 rounded-full' + > + <X className='mr-2 h-4 w-4' /> Cancel Order + </Button> + </AlertDialogTrigger> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle> + Cancel Order + </AlertDialogTitle> + <AlertDialogDescription> + Are you sure you want to cancel this order? + This action cannot be undone. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel> + No, Keep Order + </AlertDialogCancel> + <AlertDialogAction + onClick={() => handleCancelOrder(order.id)} + className='bg-red-500 hover:bg-red-600 text-white' + > + Yes, Cancel Order + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + )} + </div> + </div> + </div> + </AccordionContent> + </AccordionItem> + ))} + </Accordion> + </div> + ) : ( + <div className='text-center py-12'> + <div className='mx-auto w-24 h-24 bg-pink-100 rounded-full flex items-center justify-center mb-6'> + <Package className='h-12 w-12 text-pink-500' /> + </div> + <h2 className='text-2xl font-semibold text-gray-800 mb-4'> + No orders yet + </h2> + <p className='text-gray-600 mb-8'> + You haven't placed any orders with us yet. + </p> + <Button + onClick={() => navigate('/menu')} + className='bg-pink-500 hover:bg-pink-600 text-white rounded-full px-8 py-6 text-lg' + > + <ShoppingBag className='mr-2 h-5 w-5' /> Start Shopping + </Button> + </div> + )} + + <div className='text-center mt-8'> + <Button + variant='outline' + onClick={() => navigate('/')} + className='border-pink-300 text-pink-600 hover:bg-pink-50 rounded-full' + > + <Home className='mr-2 h-4 w-4' /> Return to Home + </Button> + </div> + </div> + ); +} -- GitLab