diff --git a/backend/controller/contact.controller.js b/backend/controller/contact.controller.js new file mode 100644 index 00000000..4c81946f --- /dev/null +++ b/backend/controller/contact.controller.js @@ -0,0 +1,74 @@ +const { z } = require("zod"); +const nodemailer = require("nodemailer"); +require("dotenv").config(); + +// data require form .env file : EMAIL_USER, EMAIL_PASS + +// Define the Zod schema for contact form validation +const contactSchema = z.object({ + mail: z.string().email(), + subject: z.string().min(5, "Subject must be at least 5 characters long."), + message: z.string().min(5, "Message must be at least 5 characters long."), +}); + +const createContactUs = async (req, res) => { + const validation = contactSchema.safeParse(req.body); + + if (!validation.success) { + console.error("Error at validation"); + return res.status(400).json({ + status: "error", + errors: validation.error.errors, + }); + } + + const { mail, subject, message } = req.body; + + try { + const transporter = nodemailer.createTransport({ + service: "gmail", + host: "smtp.gmail.com", + port: 587, + secure: false, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + tls: { + rejectUnauthorized: false, // Disable strict SSL verification + }, + }); + + const mailOptions = { + from: mail, + to: process.env.EMAIL_USER, + subject: subject, + text: message, + }; + + // Send mail with defined transport object + await new Promise((resolve, reject) => { + transporter.sendMail(mailOptions, (error, info) => { + if (error) { + console.error("Error occurred: " + error.message); + reject(error); + } + resolve(info); + }); + }); + + res.status(200).json({ + status: "success", + message: "Your contact request has been successfully received.", + }); + } catch (err) { + console.error(`Error at transport: ${err}`); + res.status(500).json({ + status: "error", + message: + "There was an error sending your message. Please try again later.", + }); + } +}; + +module.exports = { createContactUs }; diff --git a/backend/routes/contactUsRouter.js b/backend/routes/contactUsRouter.js new file mode 100644 index 00000000..9871cf8f --- /dev/null +++ b/backend/routes/contactUsRouter.js @@ -0,0 +1,7 @@ +const express = require("express"); +const router = express.Router(); +const { createContactUs } = require("../controller/contact.controller"); + +router.post("/contactus", createContactUs); + +module.exports = router; diff --git a/backend/routes/index.js b/backend/routes/index.js index 27a2b8bd..096551dd 100644 --- a/backend/routes/index.js +++ b/backend/routes/index.js @@ -1,39 +1,25 @@ const express = require("express"); -const logger = require("../config/logger"); // Import your Winston logger +const logger = require("../config/logger"); // Import Winston logger require("dotenv").config(); -const config = { - JWT_SECRET: process.env.JWT_SECRET, - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, -}; - const router = express.Router(); -let feedbackRouter; - -try { - feedbackRouter = require("./feedbackRouter"); -} catch (error) { - logger.error("Error loading feedbackRouter:", error); // Log the error with Winston - feedbackRouter = (req, res) => { - res - .status(500) - .json({ error: "Feedback functionality is currently unavailable" }); - }; -} +// Utility function to safely load modules and handle errors +const safeRequire = (modulePath, fallbackMessage) => { + try { + return require(modulePath); + } catch (error) { + logger.error(`Error loading ${modulePath}:`, error); + return (req, res) => { + res.status(500).json({ error: fallbackMessage }); + }; + } +}; -let eventRouter; -try { - eventRouter = require("./eventRouter"); -} catch (error) { - logger.error("Error loading eventRouter:", error); // Log the error with Winston - eventRouter = (req, res) => { - res - .status(500) - .json({ error: "Event functionality is currently unavailable" }); - }; -} +// Safely load routers with error handling +const feedbackRouter = safeRequire("./feedbackRouter", "Feedback functionality is currently unavailable"); +const contactUsRouter = safeRequire("./contactUsRouter", "Contact Us functionality is currently unavailable"); +const eventRouter = safeRequire("./eventRouter", "Event functionality is currently unavailable"); router.get("/", (req, res) => { return res.json({ @@ -41,17 +27,19 @@ router.get("/", (req, res) => { version: "1.0.0", endpoints: { Reservation: "/reservation", - Feedback: "/feedback", // Added feedback endpoint documentation + Feedback: "/feedback", }, documentation: "https://api-docs-url.com", }); }); router.use("/event", eventRouter); -router.use("/admin", require("./adminRouter")); +router.use("/admin", safeRequire("./adminRouter", "Admin functionality is currently unavailable")); router.use("/feedback", feedbackRouter); -router.use("/user", require("./customerRouter")); -router.use("/reservation", require("./reservationRouter")); -router.use("/newsletter", require("./newsletterRoute")); -router.use("/forgot", require("./forgotRouter")); +router.use("/user", safeRequire("./customerRouter", "User functionality is currently unavailable")); +router.use("/reservation", safeRequire("./reservationRouter", "Reservation functionality is currently unavailable")); +router.use("/newsletter", safeRequire("./newsletterRoute", "Newsletter functionality is currently unavailable")); +router.use("/forgot", safeRequire("./forgotRouter", "Forgot password functionality is currently unavailable")); +router.use("/contact", contactUsRouter); + module.exports = router; diff --git a/frontend/src/components/Pages/ContactUs.jsx b/frontend/src/components/Pages/ContactUs.jsx new file mode 100644 index 00000000..50c9e6e5 --- /dev/null +++ b/frontend/src/components/Pages/ContactUs.jsx @@ -0,0 +1,185 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { useInView } from 'react-intersection-observer'; +import chess from '../../assets/img/chess.gif'; + +const ContactUs = () => { + const { ref, inView } = useInView({ + threshold: 0.2, + triggerOnce: true, + }); + + const animationVariants = { + hidden: { opacity: 0, y: 50 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.5 } }, + }; + + // Use an environment variable for backend URL + const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000'; + const [mail, setMail] = useState(''); + const [subject, setSubject] = useState(''); + const [message, setMessage] = useState(''); + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + + // Basic client-side validation for security + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!mail || !subject || !message) { + setError('All fields are required.'); + return; + } + + if (!emailRegex.test(mail)) { + setError('Please enter a valid email address.'); + return; + } + + if (subject.length > 100) { + setError('Subject must be less than 100 characters.'); + return; + } + + if (message.length > 1000) { + setError('Message must be less than 1000 characters.'); + return; + } + + setError(null); + setIsLoading(true); + + try { + const response = await fetch(`${API_URL}/api/contact/contactus`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ mail, subject, message }), + }); + + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.status}`); + } + + setSubmitted(true); + setTimeout(() => { + setMail(''); + setSubject(''); + setMessage(''); + setSubmitted(false); + }, 4000); + } catch (error) { + setError('An error occurred while sending Mail...'); + console.error('Mail sending failed : ', error); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ +
+

+ Feel Free To Mail Us.. +

+

+ Have questions or need assistance ? Reach out to us, and we'll be + happy to help !! +

+
+ Chess +
+
+ +
+
+
+ + setMail(e.target.value)} + required + className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-[#004D43] focus:border-[#004D43]" + /> +
+
+ setSubject(e.target.value)} + required + className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-[#004D43] focus:border-[#004D43]" + /> +
+
+ +
+
+ +
+
+ {submitted && ( + + Thank you, We will reply you soon... + + )} + {error && ( + + {error} + + )} +
+
+
+
+ ); +}; + +export default ContactUs; diff --git a/frontend/src/components/Shared/Navbar.jsx b/frontend/src/components/Shared/Navbar.jsx index 260a1386..da3c47b0 100644 --- a/frontend/src/components/Shared/Navbar.jsx +++ b/frontend/src/components/Shared/Navbar.jsx @@ -20,6 +20,7 @@ const Navbar = () => { { name: 'RESERVATION', path: '/reservation' }, { name: 'BOARDGAMES', path: '/boardgame' }, { name: 'MEMBERSHIP', path: '/membership' }, // Add Membership here + { name: 'CONTACTUS', path: '/contactus'} ]; useEffect(() => { diff --git a/frontend/src/router/index.jsx b/frontend/src/router/index.jsx index 964706ca..5f4b7a74 100644 --- a/frontend/src/router/index.jsx +++ b/frontend/src/router/index.jsx @@ -22,6 +22,8 @@ import VerifyOtp from '../components/Pages/VerifyOtp'; import EmailVerify from '../components/Pages/EmailVerify'; import Membership from '../components/Membership'; import HelpAndSupport from '../components/Pages/HelpAndSupport'; +import ContactUs from '../components/Pages/ContactUs'; + const router = createBrowserRouter( createRoutesFromElements( }> @@ -41,7 +43,7 @@ const router = createBrowserRouter( } /> } /> } /> - + } /> ) );