From fffd0e63b6bf53938392cc05910ef6977d62bd5c Mon Sep 17 00:00:00 2001 From: Pushpendra Singh Date: Sat, 18 Jul 2026 11:04:10 +0530 Subject: [PATCH 1/3] created API for restaurants --- server/controllers/restaurantController.ts | 197 +++++++++++++++++++++ server/models/Booking.ts | 75 ++++++++ server/models/Restaurants.ts | 150 ++++++++++++++++ server/routes/restaurant.routes.ts | 23 +++ server/server.ts | 2 + 5 files changed, 447 insertions(+) create mode 100644 server/controllers/restaurantController.ts create mode 100644 server/models/Booking.ts create mode 100644 server/models/Restaurants.ts create mode 100644 server/routes/restaurant.routes.ts diff --git a/server/controllers/restaurantController.ts b/server/controllers/restaurantController.ts new file mode 100644 index 0000000..e0a751e --- /dev/null +++ b/server/controllers/restaurantController.ts @@ -0,0 +1,197 @@ +import { Request, Response } from "express"; +import Restaurant from "../models/Restaurants.js"; +import Booking from "../models/Booking.js"; + +// ====================================================== +// Get all restaurants +// GET /api/restaurants +// ====================================================== +export const getRestaurants = async ( + req: Request, + res: Response +): Promise => { + try { + const { + search, + cuisine, + priceRange, + featured, + page = "1", + limit = "10", + } = req.query; + + const query: any = { + status: "approved", + }; + + if (search) { + query.name = { + $regex: search, + $options: "i", + }; + } + + if (cuisine) { + query.cuisine = cuisine; + } + + if (priceRange) { + query.priceRange = priceRange; + } + + if (featured !== undefined) { + query.featured = featured === "true"; + } + + const pageNumber = Number(page); + const limitNumber = Number(limit); + + const restaurants = await Restaurant.find(query) + .populate("owner", "name email") + .sort({ createdAt: -1 }) + .skip((pageNumber - 1) * limitNumber) + .limit(limitNumber); + + const total = await Restaurant.countDocuments(query); + + res.status(200).json({ + success: true, + total, + page: pageNumber, + totalPages: Math.ceil(total / limitNumber), + restaurants, + }); + } catch (error: any) { + console.error(error); + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// ====================================================== +// Get Featured Restaurants +// GET /api/restaurants/featured +// ====================================================== +export const getFeaturedRestaurants = async ( + req: Request, + res: Response +): Promise => { + try { + const restaurants = await Restaurant.find({ + status: "approved", + featured: true, + exclusive: true, + }) + .sort({ rating: -1 }) + .limit(8); + + res.status(200).json({ + success: true, + count: restaurants.length, + restaurants, + }); + } catch (error: any) { + console.error(error); + + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// ====================================================== +// Get Restaurant By Slug +// GET /api/restaurants/:slug +// ====================================================== +export const getSlugsRestaurants = async ( + req: Request, + res: Response +): Promise => { + try { + const { slug } = req.params; + + const restaurant = await Restaurant.findOne({ + slug, + status: "approved", + }).populate("owner", "name email"); + + if (!restaurant) { + res.status(404).json({ + success: false, + message: "Restaurant not found", + }); + return; + } + + res.status(200).json({ + success: true, + restaurant, + }); + } catch (error: any) { + console.error(error); + + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// ====================================================== +// Get Active Bookings for Restaurant on Selected Date +// GET /api/restaurants/:restaurantId/bookings?date=2026-07-18 +// ====================================================== +export const getRestaurantBookings = async ( + req: Request, + res: Response +): Promise => { + try { + const { restaurantId } = req.params; + const { date } = req.query; + + if (!date) { + res.status(400).json({ + success: false, + message: "Date is required.", + }); + return; + } + + const selectedDate = new Date(date as string); + + const startOfDay = new Date(selectedDate); + startOfDay.setHours(0, 0, 0, 0); + + const endOfDay = new Date(selectedDate); + endOfDay.setHours(23, 59, 59, 999); + + const bookings = await Booking.find({ + restaurant: restaurantId, + bookingDate: { + $gte: startOfDay, + $lte: endOfDay, + }, + status: { + $in: ["pending", "confirmed"], + }, + }) + .populate("user", "name email") + .sort({ timeSlot: 1 }); + + res.status(200).json({ + success: true, + count: bookings.length, + bookings, + }); + } catch (error: any) { + console.error(error); + + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; \ No newline at end of file diff --git a/server/models/Booking.ts b/server/models/Booking.ts new file mode 100644 index 0000000..7ab9718 --- /dev/null +++ b/server/models/Booking.ts @@ -0,0 +1,75 @@ +import { Document, model, Schema, Types } from "mongoose"; + +export interface IBooking extends Document { + user: Types.ObjectId; + restaurant: Types.ObjectId; + + bookingDate: Date; + timeSlot: string; + + guests: number; + cccasion?: String; + specialRequest?: string; + + status: "pending" | "confirmed" | "cancelled" | "completed"; + + createdAt: Date; + updatedAt: Date; +} + +const BookingSchema = new Schema( + { + user: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + + restaurant: { + type: Schema.Types.ObjectId, + ref: "Restaurant", + required: true, + }, + + bookingDate: { + type: Date, + required: true, + }, + + timeSlot: { + type: String, + required: true, + }, + + guests: { + type: Number, + required: true, + min: 1, + }, + + specialRequest: { + type: String, + trim: true, + default: "", + }, + + status: { + type: String, + enum: ["pending", "confirmed", "cancelled", "completed"], + default: "pending", + }, + }, + { + timestamps: true, + versionKey: false, + } +); + +// Indexes +BookingSchema.index({ user: 1 }); +BookingSchema.index({ restaurant: 1 }); +BookingSchema.index({ bookingDate: 1 }); + +const Booking = model("Booking", BookingSchema); + +export default Booking; \ No newline at end of file diff --git a/server/models/Restaurants.ts b/server/models/Restaurants.ts new file mode 100644 index 0000000..423fb85 --- /dev/null +++ b/server/models/Restaurants.ts @@ -0,0 +1,150 @@ +import { Document, model, Schema, Types } from "mongoose"; + +export interface IRestaurants extends Document { + name: string; + slug: string; + description: string; + cuisine: string; + priceRange: "$" | "$$" | "$$$" | "$$$$"; + rating: number; + reviewCount: number; + location: string; + address: string; + image: string; + chef: string; + tags: string[]; + availableSlots: string[]; + featured: boolean; + exclusive: boolean; + owner: Types.ObjectId; + status: "pending" | "approved" | "rejected"; + totalSeats: number; + createdAt: Date; + updatedAt: Date; +} + +const RestaurantSchema = new Schema( + { + name: { + type: String, + required: true, + trim: true, + }, + + slug: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + }, + + description: { + type: String, + required: true, + trim: true, + }, + + cuisine: { + type: String, + required: true, + trim: true, + }, + + priceRange: { + type: String, + enum: ["$", "$$", "$$$", "$$$$"], + required: true, + }, + + rating: { + type: Number, + default: 0, + min: 0, + max: 5, + }, + + reviewCount: { + type: Number, + default: 0, + min: 0, + }, + + location: { + type: String, + required: true, + trim: true, + }, + + address: { + type: String, + required: true, + trim: true, + }, + + image: { + type: String, + required: true, + trim: true, + }, + + chef: { + type: String, + required: true, + trim: true, + }, + + tags: { + type: [String], + default: [], + }, + + availableSlots: { + type: [String], + default: [], + }, + + featured: { + type: Boolean, + default: false, + }, + + exclusive: { + type: Boolean, + default: false, + }, + + owner: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + + status: { + type: String, + enum: ["pending", "approved", "rejected"], + default: "pending", + }, + + totalSeats: { + type: Number, + required: true, + min: 1, + }, + }, + { + timestamps: true, + } +); + +// Remove __v from JSON response +RestaurantSchema.set("toJSON", { + transform: (_, ret: any) => { + delete ret.__v; + return ret; + }, +}); + +const Restaurant = model("Restaurant", RestaurantSchema); + +export default Restaurant; \ No newline at end of file diff --git a/server/routes/restaurant.routes.ts b/server/routes/restaurant.routes.ts new file mode 100644 index 0000000..02ed309 --- /dev/null +++ b/server/routes/restaurant.routes.ts @@ -0,0 +1,23 @@ +import { Router } from "express"; +import { + getRestaurants, + getFeaturedRestaurants, + getSlugsRestaurants, + getRestaurantBookings, +} from "../controllers/restaurantController.js"; + +const restaurantRouter = Router(); + +// Get all restaurants +restaurantRouter.get("/", getRestaurants); + +// Get featured restaurants +restaurantRouter.get("/featured", getFeaturedRestaurants); + +// Get bookings for a restaurant +restaurantRouter.get("/:restaurantId/bookings", getRestaurantBookings); + +// Get restaurant by slug +restaurantRouter.get("/:slug", getSlugsRestaurants); + +export default restaurantRouter; \ No newline at end of file diff --git a/server/server.ts b/server/server.ts index f07c7ce..de36e31 100644 --- a/server/server.ts +++ b/server/server.ts @@ -7,6 +7,7 @@ import express, { Request, Response, NextFunction } from "express"; import cors from "cors"; import connectDB from "./config/db.js"; import authRouter from "./routes/authRoutes.js"; +import restaurantRouter from "./routes/restaurant.routes.js"; const app = express(); @@ -24,6 +25,7 @@ app.get("/", (req: Request, res: Response) => { }); app.use("/api/auth", authRouter); +app.use("/api/restaurants", restaurantRouter); // Global Error Handler app.use(( From 9094e07a7dab6d1ccef8eb0af556edf7a9db966d Mon Sep 17 00:00:00 2001 From: Pushpendra Singh Date: Sat, 18 Jul 2026 11:19:43 +0530 Subject: [PATCH 2/3] Apply suggestion from code review --- server/controllers/restaurantController.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/controllers/restaurantController.ts b/server/controllers/restaurantController.ts index e0a751e..5f6e0b3 100644 --- a/server/controllers/restaurantController.ts +++ b/server/controllers/restaurantController.ts @@ -25,8 +25,13 @@ export const getRestaurants = async ( }; if (search) { + if (typeof search !== "string" || search.length > 100) { + res.status(400).json({ success: false, message: "Invalid search query" }); + return; + } + const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); query.name = { - $regex: search, + $regex: escapedSearch, $options: "i", }; } From e6bf12f32a74465614a51f3350e3622902a8f15f Mon Sep 17 00:00:00 2001 From: Pushpendra Singh Date: Sat, 18 Jul 2026 11:20:13 +0530 Subject: [PATCH 3/3] Apply suggestion from code review