From 3067091fdd59cc041030787fe895b3baf391524d Mon Sep 17 00:00:00 2001 From: "ALMAZROUEI Shamma (2021) WKIS203" <shamma.almazrouei.2021@live.rhul.ac.uk> Date: Tue, 11 Mar 2025 19:58:37 +0530 Subject: [PATCH] Build menu item page --- .../src/components/ui/card.jsx | 50 ++++ golden-crust-bakery/src/lib/data.js | 4 +- golden-crust-bakery/src/main.jsx | 5 + golden-crust-bakery/src/pages/About.jsx | 8 +- golden-crust-bakery/src/pages/Menu.jsx | 9 +- golden-crust-bakery/src/pages/MenuItem.jsx | 242 ++++++++++++++++++ 6 files changed, 308 insertions(+), 10 deletions(-) create mode 100644 golden-crust-bakery/src/components/ui/card.jsx create mode 100644 golden-crust-bakery/src/pages/MenuItem.jsx diff --git a/golden-crust-bakery/src/components/ui/card.jsx b/golden-crust-bakery/src/components/ui/card.jsx new file mode 100644 index 0000000..2985cca --- /dev/null +++ b/golden-crust-bakery/src/components/ui/card.jsx @@ -0,0 +1,50 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef(({ className, ...props }, ref) => ( + <div + ref={ref} + className={cn("rounded-xl border bg-card text-card-foreground shadow", className)} + {...props} /> +)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef(({ className, ...props }, ref) => ( + <div + ref={ref} + className={cn("flex flex-col space-y-1.5 p-6", className)} + {...props} /> +)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef(({ className, ...props }, ref) => ( + <div + ref={ref} + className={cn("font-semibold leading-none tracking-tight", className)} + {...props} /> +)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef(({ className, ...props }, ref) => ( + <div + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} /> +)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef(({ className, ...props }, ref) => ( + <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> +)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef(({ className, ...props }, ref) => ( + <div + ref={ref} + className={cn("flex items-center p-6 pt-0", className)} + {...props} /> +)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/golden-crust-bakery/src/lib/data.js b/golden-crust-bakery/src/lib/data.js index 0b91f88..2d4c8e8 100644 --- a/golden-crust-bakery/src/lib/data.js +++ b/golden-crust-bakery/src/lib/data.js @@ -77,7 +77,7 @@ export const cakeData = [ price: 149.99, category: 'Wedding', image: - 'https://www.warmoven.in/blog/wp-content/uploads/2024/07/Buttercream-Beauty.png', + 'https://i0.wp.com/coucoucake.com/wp-content/uploads/2022/04/Small-Wedding-Cake-Elegant-Romantic-coucoucake12.webp', }, { id: 9, @@ -97,7 +97,7 @@ export const cakeData = [ price: 35.99, category: 'Birthday', image: - 'https://livforcake.com/wp-content/uploads/2017/05/mint-chocolate-chip-cake-3.jpg', + 'https://www.bunsenburnerbakery.com/wp-content/uploads/2021/03/mint-chocolate-chip-cake-square-9Q2B2153.jpg', }, { id: 11, diff --git a/golden-crust-bakery/src/main.jsx b/golden-crust-bakery/src/main.jsx index d41fb02..e394c5f 100644 --- a/golden-crust-bakery/src/main.jsx +++ b/golden-crust-bakery/src/main.jsx @@ -14,6 +14,7 @@ import Contact from './pages/Contact'; import Login from './pages/Login'; import Register from './pages/Register'; import Menu from './pages/Menu'; +import MenuItem from './pages/MenuItem'; const router = createBrowserRouter([ { @@ -44,6 +45,10 @@ const router = createBrowserRouter([ path: '/menu', element: <Menu />, }, + { + path: '/menu/:id', + element: <MenuItem />, + }, ], }, ]); diff --git a/golden-crust-bakery/src/pages/About.jsx b/golden-crust-bakery/src/pages/About.jsx index 033a62f..0038ef1 100644 --- a/golden-crust-bakery/src/pages/About.jsx +++ b/golden-crust-bakery/src/pages/About.jsx @@ -68,7 +68,7 @@ export default function About() { </div> <div className='relative h-[300px] md:h-[400px] rounded-2xl overflow-hidden shadow-xl order-1 md:order-2'> <img - src='https://img.freepik.com/free-photo/front-view-friends-barista_23-2148436154.jpg?t=st=1741677372~exp=1741680972~hmac=33f200e859f92c94f56e1621df35f78b4e9cf528dddab61882f34e9d1b4f1eb4&w=1800' + src='https://images.unsplash.com/photo-1572054466274-25b4ed6d6899?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' alt='Our bakery team' className='object-cover' /> @@ -161,19 +161,19 @@ export default function About() { name: 'Emma Thompson', role: 'Founder & Head Baker', bio: 'With over 15 years of experience in pastry arts, Emma leads our team with creativity and passion.', - img: 'https://img.freepik.com/free-photo/photo-pleasant-looking-girl-has-healthy-soft-skin-dark-staright-hair_273609-18461.jpg?t=st=1741686287~exp=1741689887~hmac=116de42822039e12d85f23e97e7f8847c335abe7a252dd0dadd666fd6f5b8ff2&w=2000', + img: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', }, { name: 'Michael Chen', role: 'Pastry Chef', bio: 'A graduate of Le Cordon Bleu, Michael specializes in intricate cake decorations and flavors.', - img: 'https://img.freepik.com/free-photo/indoor-picture-cheerful-handsome-young-man-having-folded-hands-looking-directly-smiling-sincerely-wearing-casual-clothes_176532-10257.jpg?t=st=1741686456~exp=1741690056~hmac=ca8481b6e3d92b08c8be06d103df4509d66d147af808c042bafa78e93558eda4&w=2000', + img: 'https://images.unsplash.com/photo-1568602471122-7832951cc4c5?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', }, { name: 'Sophia Rodriguez', role: 'Customer Experience Manager', bio: 'Sophia ensures that every customer receives exceptional service from order to delivery.', - img: 'https://img.freepik.com/free-photo/close-up-portrait-pretty-young-woman-isolated_273609-35589.jpg?t=st=1741686360~exp=1741689960~hmac=1bc751189360c60efb55cd9d98f940487a299bbef5ef9d7fdca0364e34b30dff&w=2000', + img: 'https://images.unsplash.com/photo-1512361436605-a484bdb34b5f?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', }, ].map((member, index) => ( <div diff --git a/golden-crust-bakery/src/pages/Menu.jsx b/golden-crust-bakery/src/pages/Menu.jsx index 3554c98..f848d65 100644 --- a/golden-crust-bakery/src/pages/Menu.jsx +++ b/golden-crust-bakery/src/pages/Menu.jsx @@ -7,6 +7,7 @@ import { cakeData } from '@/lib/data'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { Link } from 'react-router-dom'; export default function Menu() { const { addToCart, initializeCart } = useCartStore(); @@ -65,7 +66,7 @@ export default function Menu() { placeholder='Search cakes...' value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} - className='pl-10 rounded-full border-pink-200 focus:border-pink-400 focus:ring-pink-400' + className='pl-10 rounded-full border-pink-200 focus-visible::border-pink-400 focus-visible:ring-pink-400' /> </div> <div className='flex flex-wrap gap-2'> @@ -92,9 +93,9 @@ export default function Menu() { {filteredCakes.map((cake) => ( <div key={cake.id} - className='bg-white rounded-xl shadow-sm border overflow-hidden transition-transform hover:scale-105 hover:shadow-lg' + className='bg-white rounded-xl shadow-sm border overflow-hidden transition-transform hover:scale-105 hover:shadow-md' > - <div className='relative h-56'> + <Link to={`${cake.id}`} className='block relative h-56'> <img src={cake.image} alt={cake.name} @@ -103,7 +104,7 @@ export default function Menu() { <div className='absolute top-2 right-2 bg-pink-100 text-pink-600 px-2 py-1 rounded-full text-xs font-medium'> {cake.category} </div> - </div> + </Link> <div className='p-6'> <div className='flex justify-between items-start mb-2'> <h3 className='text-xl font-semibold text-gray-800'> diff --git a/golden-crust-bakery/src/pages/MenuItem.jsx b/golden-crust-bakery/src/pages/MenuItem.jsx new file mode 100644 index 0000000..7c55a01 --- /dev/null +++ b/golden-crust-bakery/src/pages/MenuItem.jsx @@ -0,0 +1,242 @@ +import { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import { + ArrowLeft, + Heart, + Minus, + Plus, + ShoppingCart, + Star, +} from 'lucide-react'; + +import { useCartStore } from '@/lib/store'; +import { cakeData } from '@/lib/data'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; + +export default function MenuItem() { + const navigate = useNavigate(); + const { addToCart, initializeCart } = useCartStore(); + const [cake, setCake] = useState(null); + const [quantity, setQuantity] = useState(1); + const [isLoading, setIsLoading] = useState(true); + + const cakeId = useLocation().pathname.split('/')[2]; + + useEffect(() => { + initializeCart(); + + // Find the cake by ID + const foundCake = cakeData.find((c) => c.id === Number.parseInt(cakeId)); + if (foundCake) { + setCake(foundCake); + } else { + // Cake not found, redirect to menu + navigate('/menu'); + toast({ + title: 'Product Not Found', + description: "The cake you're looking for doesn't exist.", + variant: 'destructive', + }); + } + + setIsLoading(false); + }, [initializeCart, navigate, cakeId]); + + const handleQuantityChange = (value) => { + if (quantity + value > 0) { + setQuantity(quantity + value); + } + }; + + const handleAddToCart = () => { + if (cake) { + addToCart({ + ...cake, + quantity, + }); + + toast({ + title: 'Added to cart!', + description: `${cake.name} (${quantity}) has been added to your cart.`, + }); + } + }; + + // Related cakes (same category) + const relatedCakes = cake + ? cakeData + .filter((c) => c.category === cake.category && c.id !== cake.id) + .slice(0, 3) + : []; + + if (isLoading) { + return ( + <div className='container mx-auto px-4 py-16 text-center'> + <p>Loading product details...</p> + </div> + ); + } + + if (!cake) { + return null; + } + + return ( + <div className='container mx-auto px-4 py-8 md:py-12'> + <Button + variant='ghost' + onClick={() => navigate('/menu')} + className='mb-6 text-gray-600 hover:text-gray-800' + > + <ArrowLeft className='mr-2 h-4 w-4' /> Back to Menu + </Button> + + <div className='grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 mb-12'> + <div className='relative h-[300px] md:h-[500px] rounded-2xl overflow-hidden shadow-xl'> + <img + src={cake.image} + alt={cake.name} + className='object-cover h-full w-full' + /> + <div className='absolute top-4 right-4 bg-pink-100 text-pink-600 px-3 py-1 rounded-full text-sm font-medium'> + {cake.category} + </div> + </div> + + <div className='space-y-6'> + <div> + <h1 className='text-3xl md:text-4xl font-bold text-gray-800 mb-2'> + {cake.name} + </h1> + <div className='flex items-center mb-4'> + <div className='flex text-yellow-400 mr-2'> + {[...Array(5)].map((_, i) => ( + <Star key={i} className='h-5 w-5 fill-current' /> + ))} + </div> + <span className='text-gray-600'>(24 reviews)</span> + </div> + <p className='text-2xl font-bold text-pink-600 mb-4'> + ${cake.price.toFixed(2)} + </p> + <p className='text-gray-600 mb-6'>{cake.description}</p> + </div> + + <div className='border-t border-b border-gray-200 py-6'> + <div className='flex items-center mb-6'> + <span className='text-gray-700 mr-4'>Quantity:</span> + <div className='flex items-center'> + <Button + variant='outline' + size='icon' + onClick={() => handleQuantityChange(-1)} + disabled={quantity <= 1} + className='h-10 w-10 rounded-full' + > + <Minus className='h-4 w-4' /> + </Button> + <span className='mx-4 text-lg font-medium w-8 text-center'> + {quantity} + </span> + <Button + variant='outline' + size='icon' + onClick={() => handleQuantityChange(1)} + className='h-10 w-10 rounded-full' + > + <Plus className='h-4 w-4' /> + </Button> + </div> + </div> + + <div className='flex flex-col sm:flex-row gap-4'> + <Button + onClick={handleAddToCart} + className='bg-pink-500 hover:bg-pink-600 text-white rounded-full py-6 flex-1' + > + <ShoppingCart className='mr-2 h-5 w-5' /> Add to Cart + </Button> + <Button + variant='outline' + className='border-pink-300 text-pink-600 hover:bg-pink-50 rounded-full py-6' + > + <Heart className='mr-2 h-5 w-5' /> Add to Wishlist + </Button> + </div> + </div> + + <div className='space-y-4'> + <h3 className='text-lg font-semibold text-gray-800'> + Product Details + </h3> + <div className='grid grid-cols-2 gap-4'> + <div> + <p className='text-gray-600 font-medium'>Category</p> + <p className='text-gray-800'>{cake.category}</p> + </div> + <div> + <p className='text-gray-600 font-medium'>Serves</p> + <p className='text-gray-800'>8-10 people</p> + </div> + <div> + <p className='text-gray-600 font-medium'>Allergens</p> + <p className='text-gray-800'>Eggs, Dairy, Wheat</p> + </div> + <div> + <p className='text-gray-600 font-medium'>Storage</p> + <p className='text-gray-800'>Refrigerate</p> + </div> + </div> + </div> + </div> + </div> + + {/* Related Products */} + {relatedCakes.length > 0 && ( + <div className='mt-16'> + <h2 className='text-2xl font-bold text-gray-800 mb-6'> + You May Also Like + </h2> + <div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6'> + {relatedCakes.map((relatedCake) => ( + <Card + key={relatedCake.id} + className='overflow-hidden shadow-sm transition-transform hover:scale-105 hover:shadow-md' + > + <div className='relative h-56'> + <img + src={relatedCake.image} + alt={relatedCake.name} + className='object-cover h-full w-full' + /> + </div> + <CardContent className='p-6'> + <div className='flex justify-between items-start mb-2'> + <h3 className='text-xl font-semibold text-gray-800'> + {relatedCake.name} + </h3> + <p className='text-pink-600 font-bold'> + ${relatedCake.price.toFixed(2)} + </p> + </div> + <p className='text-gray-600 mb-4 line-clamp-2'> + {relatedCake.description} + </p> + <Button + onClick={() => navigate(`/menu/${relatedCake.id}`)} + className='w-full bg-yellow-100 hover:bg-yellow-200 text-yellow-800 rounded-full' + > + View Details + </Button> + </CardContent> + </Card> + ))} + </div> + </div> + )} + </div> + ); +} -- GitLab