From 8354631fc0af5184ace31dd379934e94c59039b6 Mon Sep 17 00:00:00 2001 From: "ALMAZROUEI Shamma (2021) WKIS203" <shamma.almazrouei.2021@live.rhul.ac.uk> Date: Tue, 11 Mar 2025 17:23:48 +0530 Subject: [PATCH] Build the contact page --- golden-crust-bakery/package-lock.json | 43 +++ golden-crust-bakery/package.json | 2 + golden-crust-bakery/src/main.jsx | 10 +- golden-crust-bakery/src/pages/Contact.jsx | 381 ++++++++++++++++++++++ 4 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 golden-crust-bakery/src/pages/Contact.jsx diff --git a/golden-crust-bakery/package-lock.json b/golden-crust-bakery/package-lock.json index 5b8061a..9d36fc4 100644 --- a/golden-crust-bakery/package-lock.json +++ b/golden-crust-bakery/package-lock.json @@ -19,8 +19,10 @@ "clsx": "^2.1.1", "lucide-react": "^0.479.0", "next-themes": "^0.4.5", + "pigeon-maps": "^0.22.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-leaflet": "^5.0.0-rc.2", "react-router-dom": "^7.3.0", "sonner": "^2.0.1", "tailwind-merge": "^3.0.2", @@ -1891,6 +1893,17 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.35.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", @@ -3480,6 +3493,13 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause", + "peer": true + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3838,6 +3858,15 @@ "node": ">=0.10.0" } }, + "node_modules/pigeon-maps": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/pigeon-maps/-/pigeon-maps-0.22.1.tgz", + "integrity": "sha512-mVWxgpIyhAekITeJvDGlFAo/Ytm6Fg7prpDiAuQ0Z6cJ/LwpnS8F0sF+0TDqyJu7C/DDHupkstFTNoIRqhdP5A==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -4051,6 +4080,20 @@ "react": "^19.0.0" } }, + "node_modules/react-leaflet": { + "version": "5.0.0-rc.2", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0-rc.2.tgz", + "integrity": "sha512-1xQGYG9mEIW+nfkQhqgHImwUuB1UDlnzYFSzv6PrBFDBeYrFmv0BbpwpNAFdJg/UQ2yz5UZSL7ZwlUxjwb8MZw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0-rc.2" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", diff --git a/golden-crust-bakery/package.json b/golden-crust-bakery/package.json index e9b80ea..d63182e 100644 --- a/golden-crust-bakery/package.json +++ b/golden-crust-bakery/package.json @@ -21,8 +21,10 @@ "clsx": "^2.1.1", "lucide-react": "^0.479.0", "next-themes": "^0.4.5", + "pigeon-maps": "^0.22.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-leaflet": "^5.0.0-rc.2", "react-router-dom": "^7.3.0", "sonner": "^2.0.1", "tailwind-merge": "^3.0.2", diff --git a/golden-crust-bakery/src/main.jsx b/golden-crust-bakery/src/main.jsx index 4a44fcb..42b3ab9 100644 --- a/golden-crust-bakery/src/main.jsx +++ b/golden-crust-bakery/src/main.jsx @@ -10,7 +10,7 @@ import './index.css'; import App from './App.jsx'; import Landing from './pages/Landing'; import About from './pages/About'; -// import Contact from './pages/Contact'; +import Contact from './pages/Contact'; import Login from './pages/Login'; import Register from './pages/Register'; @@ -27,10 +27,10 @@ const router = createBrowserRouter([ path: '/about', element: <About />, }, - // { - // path: '/contact', - // element: <Contact />, - // }, + { + path: '/contact', + element: <Contact />, + }, { path: '/login', element: <Login />, diff --git a/golden-crust-bakery/src/pages/Contact.jsx b/golden-crust-bakery/src/pages/Contact.jsx new file mode 100644 index 0000000..a7d34b5 --- /dev/null +++ b/golden-crust-bakery/src/pages/Contact.jsx @@ -0,0 +1,381 @@ +import { useState } from 'react'; +import { toast } from 'sonner'; +import { Clock, Mail, MapPin, Phone, Send } from 'lucide-react'; + +import { Map, Marker } from 'pigeon-maps'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; + +export default function Contact() { + const [formData, setFormData] = useState({ + name: '', + email: '', + phone: '', + subject: '', + message: '', + }); + + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + + // Clear error when field is being edited + if (errors[name]) { + setErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + const handleSelectChange = (value) => { + setFormData((prev) => ({ + ...prev, + subject: value, + })); + + if (errors.subject) { + setErrors((prev) => ({ + ...prev, + subject: '', + })); + } + }; + + const validateForm = () => { + const newErrors = {}; + + if (!formData.name.trim()) newErrors.name = 'Name is required'; + if (!formData.email.trim()) { + newErrors.email = 'Email is required'; + } else if (!/\S+@\S+\.\S+/.test(formData.email)) { + newErrors.email = 'Email is invalid'; + } + if (!formData.subject) newErrors.subject = 'Subject is required'; + if (!formData.message.trim()) newErrors.message = 'Message is required'; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!validateForm()) return; + + setIsSubmitting(true); + + setTimeout(() => { + setIsSubmitting(false); + + toast({ + title: 'Message Sent!', + description: "Thank you for contacting us. We'll get back to you soon.", + }); + + setFormData({ + name: '', + email: '', + phone: '', + subject: '', + message: '', + }); + }, 1500); + }; + + return ( + <div className='container mx-auto px-4 py-8 md:py-12'> + <div className='text-center mb-12'> + <h1 className='text-4xl font-bold text-gray-800 mb-4'>Contact Us</h1> + <p className='text-gray-600 max-w-2xl mx-auto'> + Have questions or special requests? We're here to help! Get in touch + with our team. + </p> + </div> + + <div className='grid grid-cols-1 lg:grid-cols-2 gap-12'> + <div> + <div className='bg-white rounded-xl border shadow-sm overflow-hidden mb-8'> + <div className='p-6'> + <h2 className='text-2xl font-bold text-gray-800 mb-6'> + Send Us a Message + </h2> + + <form onSubmit={handleSubmit} className='space-y-6'> + <div className='space-y-2'> + <Label htmlFor='name'>Full Name</Label> + <Input + id='name' + name='name' + placeholder='Peter Parker' + value={formData.name} + onChange={handleChange} + className={`rounded-lg ${ + errors.name ? 'border-red-500' : 'border-gray-200' + }`} + /> + {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' + placeholder='yourname@email.com' + value={formData.email} + onChange={handleChange} + 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 className='space-y-2'> + <Label htmlFor='phone'>Phone Number (Optional)</Label> + <Input + id='phone' + name='phone' + placeholder='(+44) 8765432109' + value={formData.phone} + onChange={handleChange} + className='rounded-lg border-gray-200' + /> + </div> + + <div className='space-y-2'> + <Label htmlFor='subject'>Subject</Label> + <Select + value={formData.subject} + onValueChange={handleSelectChange} + > + <SelectTrigger + className={`rounded-lg ${ + errors.subject ? 'border-red-500' : 'border-gray-200' + }`} + > + <SelectValue placeholder='Select a subject' /> + </SelectTrigger> + <SelectContent> + <SelectItem value='general'>General Inquiry</SelectItem> + <SelectItem value='order'>Order Question</SelectItem> + <SelectItem value='custom'> + Custom Cake Request + </SelectItem> + <SelectItem value='feedback'>Feedback</SelectItem> + <SelectItem value='other'>Other</SelectItem> + </SelectContent> + </Select> + {errors.subject && ( + <p className='text-red-500 text-sm'>{errors.subject}</p> + )} + </div> + + <div className='space-y-2'> + <Label htmlFor='message'>Your Message</Label> + <Textarea + id='message' + name='message' + placeholder='Your message here...' + value={formData.message} + onChange={handleChange} + className={`rounded-lg min-h-[150px] ${ + errors.message ? 'border-red-500' : 'border-gray-200' + }`} + /> + {errors.message && ( + <p className='text-red-500 text-sm'>{errors.message}</p> + )} + </div> + + <Button + type='submit' + disabled={isSubmitting} + className='w-full bg-pink-500 hover:bg-pink-600 text-white rounded-full py-6' + > + {isSubmitting ? ( + 'Sending...' + ) : ( + <> + <Send className='mr-2 h-4 w-4' /> Send Message + </> + )} + </Button> + </form> + </div> + </div> + </div> + + <div> + <div className='bg-white rounded-xl shadow-sm border overflow-hidden mb-8'> + <div className='p-6'> + <h2 className='text-2xl font-bold text-gray-800 mb-6'> + Contact Information + </h2> + + <div className='space-y-6'> + <div className='flex items-start'> + <div className='mr-4'> + <div className='w-10 h-10 bg-pink-100 rounded-full flex items-center justify-center'> + <MapPin className='h-5 w-5 text-pink-600' /> + </div> + </div> + <div> + <h3 className='font-semibold text-gray-800 mb-1'> + Our Location + </h3> + <p className='text-gray-600'> + Golden Crust Bakery, Bakery Street, London, UK + </p> + </div> + </div> + + <div className='flex items-start'> + <div className='mr-4'> + <div className='w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center'> + <Phone className='h-5 w-5 text-blue-600' /> + </div> + </div> + <div> + <h3 className='font-semibold text-gray-800 mb-1'> + Phone Number + </h3> + <p className='text-gray-600'>(+44) 8765432109</p> + </div> + </div> + + <div className='flex items-start'> + <div className='mr-4'> + <div className='w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center'> + <Mail className='h-5 w-5 text-yellow-600' /> + </div> + </div> + <div> + <h3 className='font-semibold text-gray-800 mb-1'> + Email Address + </h3> + <p className='text-gray-600'>info@goldencrust.com</p> + </div> + </div> + + <div className='flex items-start'> + <div className='mr-4'> + <div className='w-10 h-10 bg-green-100 rounded-full flex items-center justify-center'> + <Clock className='h-5 w-5 text-green-600' /> + </div> + </div> + <div> + <h3 className='font-semibold text-gray-800 mb-1'> + Business Hours + </h3> + <p className='text-gray-600'> + Everyday - 11:00 AM - 7:00 PM + </p> + </div> + </div> + </div> + </div> + </div> + + <div className='bg-white rounded-xl shadow-sm border overflow-hidden'> + <div className='p-6'> + <h2 className='text-2xl font-bold text-gray-800 mb-6'> + Frequently Asked Questions + </h2> + + <div className='space-y-4'> + <Accordion type='single' collapsible> + <AccordionItem value='item-1'> + <AccordionTrigger> + How far in advance should I order a cake? + </AccordionTrigger> + <AccordionContent> + We recommend placing your order at least 3-5 days in + advance for standard cakes, and 2-3 weeks for custom or + wedding cakes. + </AccordionContent> + </AccordionItem> + + <AccordionItem value='item-2'> + <AccordionTrigger>Do you offer delivery?</AccordionTrigger> + <AccordionContent> + Yes, we offer delivery within a 15-mile radius of our + bakery for a small fee. For larger orders or wedding + cakes, delivery is available at special rates. + </AccordionContent> + </AccordionItem> + + <AccordionItem value='item-3'> + <AccordionTrigger> + Can you accommodate dietary restrictions? + </AccordionTrigger> + <AccordionContent> + We offer gluten-free and vegan options for many of our + cakes. Please contact us directly to discuss your specific + dietary needs. + </AccordionContent> + </AccordionItem> + + <AccordionItem value='item-4'> + <AccordionTrigger> + How do I place a custom cake order? + </AccordionTrigger> + <AccordionContent> + You can contact us through this form, call us, or visit + our bakery in person to discuss your custom cake + requirements. + </AccordionContent> + </AccordionItem> + </Accordion> + </div> + </div> + </div> + </div> + </div> + + {/* Map Section */} + <div className='mt-12 bg-white rounded-xl shadow-md overflow-hidden'> + <div className='p-6'> + <h2 className='text-2xl font-bold text-gray-800 mb-6 text-center'> + Find Us + </h2> + <div className='relative h-[400px] bg-gray-200 rounded-lg'> + <div className='absolute inset-0 flex items-center justify-center rounded-lg overflow-hidden'> + <Map defaultCenter={[51.5072, 0.1276]} defaultZoom={16}> + <Marker width={50} anchor={[51.5072, 0.1276]} /> + </Map> + </div> + </div> + </div> + </div> + </div> + ); +} -- GitLab