implement catering inquiry system with nodemailer and add catering service pages

This commit is contained in:
Alaguraj0361 2026-04-03 17:45:00 +05:30
parent 9efd6db977
commit 617034ff08
7 changed files with 499 additions and 41 deletions

21
package-lock.json generated
View File

@ -11,6 +11,7 @@
"axios": "^1.13.2", "axios": "^1.13.2",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.24",
"next": "16.0.3", "next": "16.0.3",
"nodemailer": "^8.0.4",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-google-recaptcha": "^3.1.0", "react-google-recaptcha": "^3.1.0",
@ -21,6 +22,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^7.0.11",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/react-google-recaptcha": "^2.1.9", "@types/react-google-recaptcha": "^2.1.9",
@ -1340,6 +1342,16 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/nodemailer": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz",
"integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.7", "version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
@ -7575,6 +7587,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nodemailer": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/normalize-url": { "node_modules/normalize-url": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz",

View File

@ -14,6 +14,7 @@
"axios": "^1.13.2", "axios": "^1.13.2",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.24",
"next": "16.0.3", "next": "16.0.3",
"nodemailer": "^8.0.4",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-google-recaptcha": "^3.1.0", "react-google-recaptcha": "^3.1.0",
@ -24,6 +25,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^7.0.11",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/react-google-recaptcha": "^2.1.9", "@types/react-google-recaptcha": "^2.1.9",

View File

@ -1,18 +1,78 @@
'use server' 'use server'
import nodemailer from 'nodemailer';
// Configure SMTP transport using environment variables
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST || 'smtp.gmail.com', // Replace with actual SMTP host if different
port: Number(process.env.EMAIL_PORT) || 587,
secure: (process.env.EMAIL_SECURE === 'true'),
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
export async function submitReservation(formData: FormData) { export async function submitReservation(formData: FormData) {
const name = formData.get('name');
const phone = formData.get('phone');
const date = formData.get('date');
const message = formData.get('message');
// For reservations, we'll log it for now as per current logic
console.log('Reservation received:', { name, phone, date, message });
// Simulate delay
await new Promise(resolve => setTimeout(resolve, 1000));
return { success: true, message: 'Reservation submitted successfully!' };
}
export async function submitCateringInquiry(formData: FormData) {
const rawFormData = { const rawFormData = {
name: formData.get('name'), namhello@antalyarestaurant.ca),
email: formData.get('email'),
phone: formData.get('phone'), phone: formData.get('phone'),
eventType: formData.get('eventType'),
date: formData.get('date'), date: formData.get('date'),
guests: formData.get('guests'),
message: formData.get('message'), message: formData.get('message'),
};
try {
// Send email to hello@antalyarestaurant.ca
await transporter.sendMail({
from: process.env.EMAIL_USER || '"Antalya Website" <noreply@antalyarestaurant.ca>',
to: 'hello@antalyarestaurant.ca',
subject: `Catering Inquiry: ${rawFormData.eventType} - ${rawFormData.name}`,
text: `
Name: ${rawFormData.name}
Email: ${rawFormData.email}
Phone: ${rawFormData.phone}
Event Type: ${rawFormData.eventType}
Event Date: ${rawFormData.date}
Number of Guests: ${rawFormData.guests}
Message: ${rawFormData.message}
`,
html: `
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #441109;">
<h2 style="color: #c49c5c; border-bottom: 2px solid #c49c5c; padding-bottom: 10px;">New Catering Inquiry</h2>
<p><strong>Name:</strong> ${rawFormData.name}</p>
<p><strong>Email:</strong> ${rawFormData.email}</p>
<p><strong>Phone:</strong> ${rawFormData.phone}</p>
<p><strong>Event Type:</strong> ${rawFormData.eventType}</p>
<p><strong>Event Date:</strong> ${rawFormData.date}</p>
<p><strong>Number of Guests:</strong> ${rawFormData.guests}</p>
<div style="margin-top: 20px; padding: 15px; background: #fdfaf5; border-radius: 8px;">
<p><strong>Message:</strong></p>
<p>${rawFormData.message || 'No special requests provided.'}</p>
</div>
</div>
`,
});
return { success: true, message: 'Your catering inquiry has been submitted successfully to hello@antalyarestaurant.ca!' };
} catch (error) {
console.error('Failed to send catering inquiry email:', error);
return { success: false, message: 'There was an error sending your inquiry. Please try again or email us directly.' };
} }
// Simulate server-side processing
console.log('Reservation received:', rawFormData)
// In a real app, you would save to DB or send email here
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: 'Reservation submitted successfully!' }
} }

View File

@ -9,11 +9,19 @@ import Image from 'next/image'
import styles from './catering.module.css' import styles from './catering.module.css'
import CateringPackages from './CateringPackages'; import CateringPackages from './CateringPackages';
import CateringPopup from './CateringPopup';
export default function CateringContent() { export default function CateringContent() {
// Slider state for Visual Journey section // Slider state for Visual Journey section
const [currentSlide, setCurrentSlide] = useState(0); const [currentSlide, setCurrentSlide] = useState(0);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [selectedEventType, setSelectedEventType] = useState('');
const openPopup = (type: string) => {
setSelectedEventType(type);
setIsPopupOpen(true);
};
const sliderImages = [ const sliderImages = [
'/images/catering/visual-slider/visual-journey-1.webp', '/images/catering/visual-slider/visual-journey-1.webp',
@ -114,7 +122,12 @@ export default function CateringContent() {
return ( return (
<main className={styles.main}> <main className={styles.main}>
{/* Catering Popup Modal */}
<CateringPopup
isOpen={isPopupOpen}
onClose={() => setIsPopupOpen(false)}
initialEventType={selectedEventType}
/>
{/* Page Hero */} {/* Page Hero */}
<section className={styles.hero}> <section className={styles.hero}>
@ -149,7 +162,11 @@ export default function CateringContent() {
<div className={styles.topCardsGrid}> <div className={styles.topCardsGrid}>
{/* Card 1: Events */} {/* Card 1: Events */}
<motion.div className={styles.topCard} variants={scaleIn}> <motion.div
className={`${styles.topCard} ${styles.clickableCard}`}
variants={scaleIn}
onClick={() => openPopup('Corporate & Social Events')}
>
<div className={styles.topCardImage}> <div className={styles.topCardImage}>
<Image <Image
src="/images/catering/card/card-1.webp" src="/images/catering/card/card-1.webp"
@ -161,14 +178,15 @@ export default function CateringContent() {
</div> </div>
<div className={styles.topCardContent}> <div className={styles.topCardContent}>
<h3 className={styles.topCardTitle}>Corporate & Social Events</h3> <h3 className={styles.topCardTitle}>Corporate & Social Events</h3>
{/* <button className={styles.topCardButton}>
<span></span>
</button> */}
</div> </div>
</motion.div> </motion.div>
{/* Card 2: Food & Drinks */} {/* Card 2: Food & Drinks */}
<motion.div className={styles.topCard} variants={scaleIn}> <motion.div
className={`${styles.topCard} ${styles.clickableCard}`}
variants={scaleIn}
onClick={() => openPopup('Family Gatherings & Weddings')}
>
<div className={styles.topCardImage}> <div className={styles.topCardImage}>
<Image <Image
src="/images/catering/card/card-2.webp" src="/images/catering/card/card-2.webp"
@ -180,14 +198,15 @@ export default function CateringContent() {
</div> </div>
<div className={styles.topCardContent}> <div className={styles.topCardContent}>
<h3 className={styles.topCardTitle}>Family Gatherings & Weddings</h3> <h3 className={styles.topCardTitle}>Family Gatherings & Weddings</h3>
{/* <button className={styles.topCardButton}>
<span></span>
</button> */}
</div> </div>
</motion.div> </motion.div>
{/* Card 3: Venues */} {/* Card 3: Venues */}
<motion.div className={styles.topCard} variants={scaleIn}> <motion.div
className={`${styles.topCard} ${styles.clickableCard}`}
variants={scaleIn}
onClick={() => openPopup('Birthday Party & Baby Shower')}
>
<div className={styles.topCardImage}> <div className={styles.topCardImage}>
<Image <Image
src="/images/catering/card/card-3.webp" src="/images/catering/card/card-3.webp"
@ -199,9 +218,6 @@ export default function CateringContent() {
</div> </div>
<div className={styles.topCardContent}> <div className={styles.topCardContent}>
<h3 className={styles.topCardTitle}>Birthday Party & Baby Shower</h3> <h3 className={styles.topCardTitle}>Birthday Party & Baby Shower</h3>
{/* <button className={styles.topCardButton}>
<span></span>
</button> */}
</div> </div>
</motion.div> </motion.div>
</div> </div>
@ -274,13 +290,6 @@ export default function CateringContent() {
<Image src="/images/eat.png" alt="Catering Visualization Decorative Cutlery Icon" width={24} height={24} /> <Image src="/images/eat.png" alt="Catering Visualization Decorative Cutlery Icon" width={24} height={24} />
</div> </div>
<h2 className={styles.mainHeadingSection}>A Visual Journey Through Antalya Catering</h2> <h2 className={styles.mainHeadingSection}>A Visual Journey Through Antalya Catering</h2>
{/* <div className={styles.welcomeDivider}>
<svg width="120" height="20" viewBox="0 0 120 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0" y1="10" x2="45" y2="10" stroke="#d3cab3" strokeWidth="2" />
<path d="M52 5 Q60 10 52 15 M55 5 Q63 10 55 15 M58 5 Q66 10 58 15" stroke="#d3cab3" strokeWidth="1.5" fill="none" />
<line x1="75" y1="10" x2="120" y2="10" stroke="#d3cab3" strokeWidth="2" />
</svg>
</div> */}
<p className={styles.welcomeDescription}> <p className={styles.welcomeDescription}>
Experience the essence of Antalya brought to your event - from beautifully presented dishes to elegant setups that elevate every occasion. Our catering blends authentic Turkish flavours with refined presentation, creating a feast that delights both the eyes and the palate. Experience the essence of Antalya brought to your event - from beautifully presented dishes to elegant setups that elevate every occasion. Our catering blends authentic Turkish flavours with refined presentation, creating a feast that delights both the eyes and the palate.
</p> </p>
@ -372,11 +381,6 @@ export default function CateringContent() {
> >
hello@antalyarestaurant.ca hello@antalyarestaurant.ca
</motion.a> </motion.a>
{/* <motion.button className={styles.storyButton} variants={fadeInUp}>
<span className={styles.storyButtonIcon}></span>
<span>Visit site</span>
</motion.button> */}
</motion.div> </motion.div>
</section> </section>

View File

@ -0,0 +1,238 @@
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(8px);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 20px;
overflow-y: auto; /* Enable overlay-level scrolling */
}
.popup {
background-color: #f5e6d3;
padding: 3rem;
border-radius: 20px;
width: 95%;
max-width: 750px;
position: relative;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(196, 156, 92, 0.4);
margin: auto; /* Center when scrollable */
max-height: calc(100vh - 40px);
overflow-y: auto; /* Scrollable content inside the popup */
}
/* Custom scrollbar for the popup */
.popup::-webkit-scrollbar {
width: 6px;
}
.popup::-webkit-scrollbar-track {
background: transparent;
}
.popup::-webkit-scrollbar-thumb {
background: #c49c5c;
border-radius: 10px;
}
.closeButton {
position: absolute;
top: 1rem;
right: 1rem;
background: #fff;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #441109;
transition: transform 0.3s ease;
z-index: 10;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.closeButton:hover {
transform: rotate(90deg) scale(1.1);
}
.title {
font-family: var(--font-playfair);
font-size: 2.2rem;
color: #441109;
margin-bottom: 0.5rem;
text-align: center;
}
.subtitle {
font-size: 0.95rem;
color: #5d4037;
margin-bottom: 2.5rem;
text-align: center;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.formGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.2rem;
}
.inputGroup {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.fullWidth {
grid-column: span 2;
}
.label {
font-size: 0.8rem;
font-weight: 700;
color: #441109;
text-transform: uppercase;
letter-spacing: 1px;
}
.input, .textarea {
padding: 0.8rem 1rem;
border-radius: 10px;
border: 1px solid #d3cab3;
background-color: #fff;
color: #441109;
font-family: inherit;
font-size: 0.95rem;
outline: none;
transition: all 0.3s ease;
}
.input:focus, .textarea:focus {
border-color: #c49c5c;
box-shadow: 0 0 0 4px rgba(196, 156, 92, 0.15);
}
.textarea {
resize: none;
height: 100px;
grid-column: span 2;
}
.submitButton {
background-color: #441109;
color: #f5e6d3;
padding: 1.1rem;
border-radius: 10px;
border: 1px solid #441109;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2.5px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 0.5rem;
font-size: 0.95rem;
grid-column: span 2;
}
.submitButton:hover {
background-color: #c49c5c;
border-color: #c49c5c;
color: #fff;
}
.submitButton:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.successMessage {
background-color: rgba(76, 175, 80, 0.1);
color: #2e7d32;
padding: 1rem;
border-radius: 8px;
text-align: center;
font-size: 0.9rem;
font-weight: 600;
}
@media (max-width: 768px) {
.formGrid {
grid-template-columns: 1fr;
gap: 1rem;
}
.textarea, .submitButton, .fullWidth {
grid-column: span 1;
}
.popup {
padding: 2.5rem 1.5rem 2rem;
max-height: calc(100vh - 40px);
}
.title {
font-size: 1.8rem;
}
.subtitle {
margin-bottom: 1.5rem;
font-size: 0.9rem;
}
.input, .textarea {
padding: 0.7rem 0.9rem;
}
}
@media (max-width: 480px) {
.popup {
padding: 2.5rem 1.2rem 1.5rem;
width: 100%;
border-radius: 15px;
}
.title {
font-size: 1.5rem;
}
.subtitle {
font-size: 0.85rem;
}
.form {
gap: 0.8rem;
}
.inputGroup {
gap: 0.3rem;
}
.label {
font-size: 0.75rem;
}
.submitButton {
padding: 0.9rem;
font-size: 0.9rem;
margin-top: 0.2rem;
}
}

View File

@ -0,0 +1,133 @@
'use client'
import { useState, useTransition } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { submitCateringInquiry } from '../actions'
import styles from './CateringPopup.module.css'
interface CateringPopupProps {
isOpen: boolean;
onClose: () => void;
initialEventType?: string;
}
export default function CateringPopup({ isOpen, onClose, initialEventType = '' }: CateringPopupProps) {
const [isPending, startTransition] = useTransition();
const [isSuccess, setIsSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setError(null);
const formData = new FormData(event.currentTarget);
startTransition(async () => {
try {
const result = await submitCateringInquiry(formData);
if (result.success) {
setIsSuccess(true);
setTimeout(() => {
onClose();
setIsSuccess(false);
}, 3000);
} else {
setError('Failed to submit inquiry. Please try again.');
}
} catch (err) {
setError('Something went wrong. Please try again later.');
}
});
}
return (
<AnimatePresence>
{isOpen && (
<motion.div
className={styles.overlay}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={(e) => e.target === e.currentTarget && onClose()}
>
<motion.div
className={styles.popup}
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
>
<button className={styles.closeButton} onClick={onClose}></button>
<h2 className={styles.title}>Catering Inquiry</h2>
<p className={styles.subtitle}>Let us make your event unforgettable with authentic Turkish flavors.</p>
{isSuccess ? (
<motion.div
className={styles.successMessage}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
>
Thank you! Your inquiry has been sent to hello@antalyarestaurant.ca. We will get back to you shortly.
</motion.div>
) : (
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.formGrid}>
<div className={styles.inputGroup}>
<label className={styles.label} htmlFor="name">Full Name</label>
<input className={styles.input} type="text" id="name" name="name" required placeholder="Enter Name" />
</div>
<div className={styles.inputGroup}>
<label className={styles.label} htmlFor="email">Email Address</label>
<input className={styles.input} type="email" id="email" name="email" required placeholder="Enter Email" />
</div>
<div className={styles.inputGroup}>
<label className={styles.label} htmlFor="phone">Phone Number</label>
<input className={styles.input} type="tel" id="phone" name="phone" required placeholder="Enter Phone Number" />
</div>
<div className={styles.inputGroup}>
<label className={styles.label} htmlFor="eventType">Event Type</label>
<select className={styles.input} id="eventType" name="eventType" defaultValue={initialEventType} required>
<option value="" disabled>Select Event Type</option>
<option value="Corporate & Social Events">Corporate & Social Events</option>
<option value="Family Gatherings & Weddings">Family Gatherings & Weddings</option>
<option value="Birthday Party & Baby Shower">Birthday Party & Baby Shower</option>
<option value="Other">Other Event</option>
</select>
</div>
<div className={styles.inputGroup}>
<label className={styles.label} htmlFor="date">Date of Event</label>
<input className={styles.input} type="date" id="date" name="date" required />
</div>
<div className={styles.inputGroup}>
<label className={styles.label} htmlFor="guests">Number of Guests</label>
<input className={styles.input} type="number" id="guests" name="guests" required min="1" placeholder="Enter Number of Guests" />
</div>
<div className={`${styles.inputGroup} ${styles.fullWidth}`}>
<label className={styles.label} htmlFor="message">Any Special Requests?</label>
<textarea className={styles.textarea} id="message" name="message" placeholder="Enter Your Message..."></textarea>
</div>
<button
className={styles.submitButton}
type="submit"
disabled={isPending}
>
{isPending ? 'Sending Inquiry...' : 'Submit Inquiry'}
</button>
</div>
{error && <p style={{ color: 'red', fontSize: '0.85rem', textAlign: 'center' }}>{error}</p>}
</form>
)}
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}

View File

@ -169,10 +169,10 @@
cursor: pointer; cursor: pointer;
transition: transform 0.4s ease, box-shadow 0.4s ease; transition: transform 0.4s ease, box-shadow 0.4s ease;
border-radius: 30 30 30 30px; border-radius: 30 30 30 30px;
/* Rounded only at Bottom-Left to emphasize L shape */ }
border: none;
border-right: 8px solid #b07c4b; .clickableCard {
border-top: 8px solid #b07c4b; cursor: pointer;
} }
.topCard:hover { .topCard:hover {