- {orders.map((order,index)=>(
-
-

-
-
- {order.items.map((item,index)=>{
- if (index===order.items.length-1){
- return item.name + " x " + item.quantity
- }
- else{
- return item.name + " x " + item.quantity + ", "
- }
- })}
-
-
{order.address.firstName+" "+order.address.lastName}
-
-
{order.address.street+","}
-
{order.address.city+", "+order.address.state+", "+order.address.country+", "+order.address.zipcode}
+ {orders.map((order, index) => {
+ const items = Array.isArray(order?.items) ? order.items : []
+ const addr = order?.address || {}
+ const fullName = [addr.firstName, addr.lastName].filter(Boolean).join(' ') || 'Unknown'
+ const street = addr.street ? `${addr.street},` : ''
+ const cityLine = [addr.city, addr.state, addr.country, addr.zipcode].filter(Boolean).join(', ')
+ const phone = addr.phone || 'N/A'
+ const amount = Number(order?.amount ?? 0)
+ const statusValue = order?.status || 'Food Processing'
+ const deliveryPhase = order?.deliveryPhase || (statusValue?.toLowerCase().includes('delivered') ? 'delivered' : statusValue?.toLowerCase().includes('out for delivery') ? 'delivering' : 'at_restaurant')
+ const deliveryType = order?.deliveryType || order?.subOrders?.[0]?.deliveryType || ''
+ const statusClass =
+ deliveryPhase === 'delivered'
+ ? 'status-delivered'
+ : deliveryPhase === 'delivering'
+ ? 'status-out'
+ : 'status-processing'
+
+ const orderKey = order?._id || index
+ const isOpen = openStatusId === orderKey
+
+ const statusLabel =
+ deliveryPhase === 'at_restaurant'
+ ? (t("statusFoodProcessing") || 'Food Processing')
+ : deliveryPhase === 'delivering'
+ ? (t("statusOutForDelivery") || 'Out For Delivery')
+ : (t("statusDelivered") || 'Delivered')
+ const draft = drafts[orderKey] || {}
+
+ const displayItemName = (item) => {
+ const vi = item?.name
+ const en = item?.nameEn
+ const dict = foodDict[item?.foodId]
+ const dictVi = dict?.name
+ const dictEn = dict?.nameEn
+ if (lang === "vi") return dictVi || vi || dictEn || en || "Item"
+ return dictEn || en || dictVi || vi || "Item"
+ }
+
+ return (
+
+
+
#{orderKey?.toString().slice(-6)}
+
+
+
+ {PHASES.map((opt) => {
+ let optClass = 'status-processing'
+ if (opt.key === 'delivering') optClass = 'status-out'
+ else if (opt.key === 'delivered') optClass = 'status-delivered'
+ const label =
+ opt.key === 'at_restaurant'
+ ? (t("statusFoodProcessing") || opt.label)
+ : opt.key === 'delivering'
+ ? (t("statusOutForDelivery") || opt.label)
+ : (t("statusDelivered") || opt.label)
+ return (
+
+ )
+ })}
+
+ {deliveryType ? (
+
+ {deliveryType === 'drone'
+ ? (t("Delivery Method (DRONE)") || "Vận Chuyển (Drone)")
+ : (t("Delivery Method (DRIVER)") || "Vận Chuyển (Driver)")}
+
+ ) : null}
+
-
{order.address.phone}
+
+
+
+

+
+
+ {items.map((item, i) => {
+ const text = `${displayItemName(item)} x ${item?.quantity ?? 0}`
+ return i === items.length - 1 ? text : `${text}, `
+ })}
+
+ {fullName ? (
+
+ {t("name")}: {fullName}
+
+ ) : null}
+ {(street || cityLine) ? (
+
+
{t("addressLabel")}:
+ {street ?
{street}
: null}
+ {cityLine ?
{cityLine}
: null}
+
+ ) : null}
+ {phone ? (
+
+ {t("phone")}: {phone}
+
+ ) : null}
+
+
+
+
+
{t("itemsLabel")}: {items.length}
+
{t("total")}: {formatMoney(amount)}
+
+
+
+ {deliveryPhase !== 'delivered' && (
+
+ {deliveryType !== 'driver' && (
+
+
+ onChangeDraft(orderKey, 'droneId', e.target.value)}
+ placeholder="VD: DR-001"
+ />
+
+ )}
+ {deliveryType !== 'drone' && (
+
+
+ onChangeDraft(orderKey, 'driverId', e.target.value)}
+ placeholder="VD: TX-123"
+ />
+
+ )}
+
+
+
+
+
+ )}
-
Items : {order.items.length}
-
${order.amount}
-
-
- ))}
+ )
+ })}
)
}
-export default Orders
\ No newline at end of file
+export default Orders
diff --git a/admin/src/ui/useUI.jsx b/admin/src/ui/useUI.jsx
new file mode 100644
index 0000000..ba683cf
--- /dev/null
+++ b/admin/src/ui/useUI.jsx
@@ -0,0 +1,206 @@
+import { createContext, useContext, useEffect, useMemo, useState } from "react";
+
+const STORAGE_KEY = "admin_ui";
+
+const translations = {
+ vi: {
+ adminPortal: "Bảng điều khiển",
+ menu: "Menu",
+ addItem: "Thêm món",
+ orders: "Đơn hàng",
+ adminView: "Chế độ Admin",
+ logout: "Đăng xuất",
+ confirmLogout: "Bạn có chắc muốn đăng xuất?",
+ loggedOut: "Đã đăng xuất",
+ toggleTheme: "Chuyển chế độ sáng/tối",
+ toggleLang: "Chuyển EN/VI",
+ allFoods: "Danh sách món",
+ image: "Hình",
+ name: "Tên",
+ category: "Danh mục",
+ price: "Giá",
+ action: "Thao tác",
+ allRestaurants: "Tất cả nhà hàng",
+ remove: "Xóa",
+ loading: "Đang tải...",
+ noPermission: "Tài khoản này không có quyền truy cập admin/owner",
+ loginSuccess: "Đăng nhập thành công",
+ loginFailed: "Đăng nhập thất bại",
+ loginAgain: "Đăng nhập lại",
+ orderPage: "Trang đơn hàng",
+ itemsLabel: "Số món",
+ phone: "Điện thoại",
+ addressLabel: "Địa chỉ",
+ statusUpdated: "Cập nhật trạng thái",
+ restaurantFilter: "Nhà hàng",
+ addPage: "Thêm món",
+ uploadImage: "Tải hình",
+ restaurant: "Nhà hàng",
+ productNameVi: "Tên món (VI)",
+ productNameEn: "Tên món (EN)",
+ descriptionEn: "Mô tả (EN)",
+ descriptionVi: "Mô tả (VI)",
+ category: "Danh mục",
+ price: "Giá",
+ addButton: "Thêm",
+ total: "Tổng",
+ statusFoodProcessing: "Đang Chế Biến",
+ statusOutForDelivery: "Đang Giao",
+ statusDelivered: "Đã Giao",
+ categorySalad: "Salad",
+ categoryRolls: "Cuốn",
+ categoryDeserts: "Tráng miệng",
+ categorySandwich: "Bánh mì kẹp",
+ categoryCake: "Bánh ngọt",
+ categoryPureVeg: "Món chay",
+ categoryPasta: "Mì Ý",
+ categoryNoodles: "Mì/Phở",
+ },
+ en: {
+ adminPortal: "Admin Portal",
+ menu: "Menu",
+ addItem: "Add Item",
+ orders: "Orders",
+ adminView: "Admin View",
+ logout: "Logout",
+ confirmLogout: "Are you sure you want to logout?",
+ loggedOut: "Logged out",
+ toggleTheme: "Toggle light/dark",
+ toggleLang: "Toggle EN/VI",
+ allFoods: "All foods",
+ image: "Image",
+ name: "Name",
+ category: "Category",
+ price: "Price",
+ action: "Action",
+ allRestaurants: "All restaurants",
+ remove: "Remove",
+ loading: "Loading...",
+ noPermission: "This account cannot access admin/owner area",
+ loginSuccess: "Login success",
+ loginFailed: "Login failed",
+ loginAgain: "Login again",
+ orderPage: "Order page",
+ itemsLabel: "Items",
+ phone: "Phone",
+ addressLabel: "Address",
+ statusUpdated: "Status updated",
+ restaurantFilter: "Restaurant",
+ addPage: "Add item",
+ uploadImage: "Upload image",
+ restaurant: "Restaurant",
+ productNameVi: "Product Name (VI)",
+ productNameEn: "Product Name (EN)",
+ descriptionEn: "Description (EN)",
+ descriptionVi: "Description (VI)",
+ category: "Category",
+ price: "Price",
+ addButton: "Add",
+ total: "Total",
+ statusFoodProcessing: "Food Processing",
+ statusOutForDelivery: "Out For Delivery",
+ statusDelivered: "Delivered",
+ categorySalad: "Salad",
+ categoryRolls: "Rolls",
+ categoryDeserts: "Desserts",
+ categorySandwich: "Sandwich",
+ categoryCake: "Cake",
+ categoryPureVeg: "Pure Veg",
+ categoryPasta: "Pasta",
+ categoryNoodles: "Noodles",
+ },
+};
+
+const UIContext = createContext(null);
+
+const RATE_USD_VND = 24000;
+
+const loadUI = () => {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return { theme: "dark", lang: "vi" };
+ const parsed = JSON.parse(raw);
+ return {
+ theme: parsed.theme || "dark",
+ lang: parsed.lang || "vi",
+ };
+ } catch {
+ return { theme: "dark", lang: "vi" };
+ }
+};
+
+export const UIProvider = ({ children }) => {
+ const [{ theme, lang }, setUI] = useState(loadUI);
+
+ useEffect(() => {
+ document.documentElement.setAttribute("data-theme", theme);
+ }, [theme]);
+
+ const toggleTheme = () =>
+ setUI((prev) => {
+ const next = prev.theme === "dark" ? "light" : "dark";
+ const data = { ...prev, theme: next };
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
+ return data;
+ });
+
+ const toggleLang = () =>
+ setUI((prev) => {
+ const next = prev.lang === "vi" ? "en" : "vi";
+ const data = { ...prev, lang: next };
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
+ return data;
+ });
+
+ const t = (key) => translations[lang]?.[key] || key;
+
+ const formatMoney = (value) => {
+ const num = Number(value || 0);
+ if (lang === "vi") {
+ return num.toLocaleString("vi-VN", {
+ style: "currency",
+ currency: "VND",
+ maximumFractionDigits: 0,
+ });
+ }
+ const usd = num / RATE_USD_VND;
+ return usd.toLocaleString("en-US", {
+ style: "currency",
+ currency: "USD",
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ });
+ };
+
+ const translateCategory = (cat) => {
+ const key = String(cat || "").toLowerCase();
+ const dict = {
+ salad: "categorySalad",
+ rolls: "categoryRolls",
+ deserts: "categoryDeserts",
+ sandwich: "categorySandwich",
+ cake: "categoryCake",
+ "pure veg": "categoryPureVeg",
+ pasta: "categoryPasta",
+ noodles: "categoryNoodles",
+ };
+ const tKey = dict[key];
+ if (tKey) return t(tKey);
+ return cat || "";
+ };
+
+ const value = useMemo(
+ () => ({ theme, lang, toggleTheme, toggleLang, t, formatMoney, translateCategory }),
+ [theme, lang]
+ );
+
+ return
{children};
+};
+
+const useUI = () => {
+ const ctx = useContext(UIContext);
+ if (!ctx) throw new Error("useUI must be used inside UIProvider");
+ return ctx;
+};
+
+export default useUI;
diff --git a/backend/config/cloudinary.js b/backend/config/cloudinary.js
new file mode 100644
index 0000000..b07c09c
--- /dev/null
+++ b/backend/config/cloudinary.js
@@ -0,0 +1,26 @@
+import dotenv from "dotenv";
+dotenv.config();
+
+import { v2 as cloudinary } from "cloudinary";
+
+export function setupCloudinary() {
+ const cloudName = process.env.CLOUDINARY_CLOUD_NAME;
+ const apiKey = process.env.CLOUDINARY_API_KEY;
+ const apiSecret = process.env.CLOUDINARY_API_SECRET;
+ const enabled = Boolean(cloudName && apiKey && apiSecret);
+
+ if (enabled) {
+ cloudinary.config({
+ cloud_name: cloudName,
+ api_key: apiKey,
+ api_secret: apiSecret,
+ });
+ console.log("[Cloudinary] Configured (using Cloudinary).");
+ } else {
+ console.warn("[Cloudinary] Missing credentials → fallback to local uploads");
+ }
+
+ return { cloudinary, enabled };
+}
+
+export default cloudinary;
\ No newline at end of file
diff --git a/backend/config/multerConfig.js b/backend/config/multerConfig.js
new file mode 100644
index 0000000..805fb74
--- /dev/null
+++ b/backend/config/multerConfig.js
@@ -0,0 +1,16 @@
+// backend/config/multerConfig.js
+import multer from "multer";
+import path from "path";
+
+const storage = multer.diskStorage({
+ destination: (req, file, cb) => {
+ cb(null, "uploads"); // Đường dẫn tới thư mục lưu trữ tạm thời
+ },
+ filename: (req, file, cb) => {
+ cb(null, `${Date.now()}${path.extname(file.originalname)}`); // Đặt tên file
+ },
+});
+
+const upload = multer({ storage });
+
+export default upload;
\ No newline at end of file
diff --git a/backend/controllers/cityController.js b/backend/controllers/cityController.js
new file mode 100644
index 0000000..411885f
--- /dev/null
+++ b/backend/controllers/cityController.js
@@ -0,0 +1,27 @@
+import cityModel from "../models/cityModel.js";
+
+const addCity = async (req, res) => {
+ try {
+ const { name, nameEn, code, active = true } = req.body;
+ if (!name || !code) {
+ return res.status(400).json({ success: false, message: "Missing name/code" });
+ }
+ const city = await cityModel.create({ name, nameEn, code, active });
+ return res.status(201).json({ success: true, data: city });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+const listCities = async (_req, res) => {
+ try {
+ const cities = await cityModel.find({}).sort({ name: 1 });
+ return res.json({ success: true, data: cities });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+export { addCity, listCities };
diff --git a/backend/controllers/foodController.js b/backend/controllers/foodController.js
index 2c2a61d..6e741f3 100644
--- a/backend/controllers/foodController.js
+++ b/backend/controllers/foodController.js
@@ -1,53 +1,192 @@
-import foodModel from '../models/foodModel.js'
-import fs from 'fs'
+import foodModel from "../models/foodModel.js";
+import restaurantModel from "../models/restaurantModel.js";
+import fs from "fs";
+import path from "path";
+import { setupCloudinary } from "../config/cloudinary.js";
+import { filterForOwner } from "../middleware/access.js";
+const { cloudinary, enabled: cloudEnabled } = setupCloudinary();
+
+// Helper: lấy public_id từ Cloudinary secure_url
+function getCloudinaryPublicIdFromUrl(urlStr) {
+ try {
+ const u = new URL(urlStr);
+ const parts = u.pathname.split("/").filter(Boolean); // ["
","image","upload","v123","folder","name.ext"]
+ const uploadIdx = parts.findIndex((p) => p === "upload");
+ if (uploadIdx === -1) return null;
+ // Bỏ qua 'image','upload' và optional 'v123'
+ let afterUpload = parts.slice(uploadIdx + 1);
+ if (afterUpload[0] && /^v\d+$/i.test(afterUpload[0])) {
+ afterUpload = afterUpload.slice(1);
+ }
+ const publicIdWithExt = afterUpload.join("/");
+ // Bỏ phần mở rộng (.jpg/.png/...)
+ return publicIdWithExt.replace(/\.[^.]+$/, "");
+ } catch {
+ return null;
+ }
+}
+
+const parseTags = (tags) => {
+ if (Array.isArray(tags)) return tags;
+ if (typeof tags === "string" && tags.length) {
+ return tags.split(",").map((t) => t.trim()).filter(Boolean);
+ }
+ return [];
+};
// add food item
+const addFood = async (req, res) => {
+ try {
+ if (!req.file) {
+ return res.status(400).json({ success: false, message: "Image is required" });
+ }
+ const {
+ name,
+ nameEn,
+ description,
+ descriptionVi,
+ category,
+ restaurantId,
+ cityId,
+ tags,
+ } = req.body;
+ const price = Number(req.body.price);
+
+ const trimmedName = name?.trim();
+ const trimmedNameEn = nameEn?.trim();
+ const trimmedDesc = description?.trim();
+ const trimmedDescVi = descriptionVi?.trim();
-const addFood = async (req,res) => {
+ if (
+ !trimmedName ||
+ !trimmedNameEn ||
+ !trimmedDesc ||
+ !trimmedDescVi ||
+ !category ||
+ Number.isNaN(price)
+ ) {
+ return res.status(400).json({ success: false, message: "Missing required fields" });
+ }
- let image_filename = `${req.file.filename}`;
+ // Kiểm tra nhà hàng hoạt động
+ if (restaurantId) {
+ const rest = await restaurantModel.findById(restaurantId);
+ if (!rest || rest.active === false) {
+ return res.status(400).json({ success: false, message: "Restaurant inactive or not found" });
+ }
+ }
- const food = new foodModel({
- name:req.body.name,
- description:req.body.description,
- price:req.body.price,
- category:req.body.category,
- image:image_filename
- })
- try {
- await food.save();
- res.json({success:true,message:"Food Added"})
- } catch (error) {
- console.log(error)
- res.json({success:false,message:"Error"})
+ let imageField = null;
+
+ if (req.file.path && cloudEnabled && cloudinary) {
+ try {
+ const uploaded = await cloudinary.uploader.upload(req.file.path, {
+ folder: "foodfast/foods",
+ resource_type: "image",
+ overwrite: true,
+ });
+ imageField = uploaded.secure_url;
+ } finally {
+ try {
+ fs.unlinkSync(req.file.path);
+ } catch {}
+ }
}
-}
-// all food list
-const listFood = async (req,res) => {
- try {
- const foods = await foodModel.find({});
- res.json({success:true,data:foods})
- } catch (error) {
- console.log(error);
- res.json({success:false,message:"Error"})
+ if (!imageField) {
+ imageField = req.file.filename;
}
-}
+
+ const food = new foodModel({
+ name: trimmedName,
+ nameEn: trimmedNameEn,
+ description: trimmedDesc,
+ descriptionVi: trimmedDescVi,
+ price,
+ category,
+ image: imageField,
+ restaurantId: restaurantId || null,
+ cityId: cityId || null,
+ tags: parseTags(tags),
+ });
+
+ await food.save();
+ return res.status(201).json({ success: true, message: "Food Added", data: food });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+// all food list (optional filter by city/restaurant/category)
+const listFood = async (req, res) => {
+ try {
+ const baseFilter = {};
+ if (req.query.cityId) baseFilter.cityId = req.query.cityId;
+ if (req.query.restaurantId) baseFilter.restaurantId = req.query.restaurantId;
+ if (req.query.category) baseFilter.category = req.query.category;
+
+ const filter = filterForOwner(req.user, baseFilter);
+ const isAdmin = req.user?.role === "admin";
+
+ if (!isAdmin) {
+ const restaurantQuery = {};
+ if (filter.cityId) restaurantQuery.cityId = filter.cityId;
+ if (filter.restaurantId) restaurantQuery._id = filter.restaurantId;
+ restaurantQuery.active = true;
+
+ // Kết hợp với allowed ids (nếu filterForOwner đã set $in)
+ if (filter.restaurantId && typeof filter.restaurantId === "object" && filter.restaurantId.$in) {
+ restaurantQuery._id = { $in: filter.restaurantId.$in };
+ }
+
+ const activeRestaurants = await restaurantModel.find(restaurantQuery, { _id: 1 });
+ const allowedIds = activeRestaurants.map((r) => r._id);
+ if (!allowedIds.length) {
+ return res.json({ success: true, data: [] });
+ }
+ filter.restaurantId = { $in: allowedIds };
+ }
+
+ const foods = await foodModel.find(filter);
+ return res.json({ success: true, data: foods });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
// remove food item
-const removeFood = async (req,res) => {
- try {
- const food = await foodModel.findById(req.body.id);
- fs.unlink(`uploads/${food.image}`,()=>{})
+const removeFood = async (req, res) => {
+ try {
+ const food = await foodModel.findById(req.body.id);
+ if (!food) return res.json({ success: false, message: "Not found" });
+
+ const isUrl = typeof food.image === "string" && /^https?:\/\//i.test(food.image);
- await foodModel.findByIdAndDelete(req.body.id);
- res.json({success:true,message:"Food Removed"})
- } catch (error) {
- console.log(error);
- res.json({success:false,message:"Error"})
+ if (isUrl) {
+ // Ảnh Cloudinary => xóa trên Cloudinary
+ if (cloudEnabled && cloudinary) {
+ const publicId = getCloudinaryPublicIdFromUrl(food.image);
+ if (publicId) {
+ try {
+ await cloudinary.uploader.destroy(publicId);
+ } catch {}
+ }
+ }
+ } else if (food.image) {
+ // Ảnh local => xóa file trong uploads/
+ const localPath = path.join(process.cwd(), "uploads", food.image);
+ fs.unlink(localPath, () => {});
}
-}
+ await foodModel.findByIdAndDelete(req.body.id);
+ return res.json({ success: true, message: "Food Removed" });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
-export {addFood,listFood,removeFood}
\ No newline at end of file
+export { addFood, listFood, removeFood };
diff --git a/backend/controllers/orderController.js b/backend/controllers/orderController.js
index 7677cdb..6d06a9b 100644
--- a/backend/controllers/orderController.js
+++ b/backend/controllers/orderController.js
@@ -1,113 +1,484 @@
import orderModel from "../models/orderModel.js";
-import userModel from "../models/userModel.js"
-import Stripe from "stripe"
-
-const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
-
-
-// placing user order from frontend
-const placeOrder = async (req,res) => {
-
- const frontend_url = "https://hangry-frontend.onrender.com";
-
- try {
- const newOrder = new orderModel({
- userId:req.body.userId,
- items:req.body.items,
- amount:req.body.amount,
- address:req.body.address
- })
- await newOrder.save();
- await userModel.findByIdAndUpdate(req.body.userId,{cartData:{}});
-
- const line_items = req.body.items.map((item)=>({
- price_data:{
- currency:"pkr",
- product_data:{
- name:item.name
- },
- unit_amount:item.price*100*275
- },
- quantity:item.quantity
- }))
-
- line_items.push({
- price_data:{
- currency:"pkr",
- product_data:{
- name:"Delivery Charges"
- },
- unit_amount:2*100*275
- },
- quantity:1
- })
-
- const session = await stripe.checkout.sessions.create({
- line_items:line_items,
- mode:'payment',
- success_url:`${frontend_url}/verify?success=true&orderId=${newOrder._id}`,
- cancel_url:`${frontend_url}/verify?success=false&orderId=${newOrder._id}`,
- })
-
- res.json({success:true,session_url:session.url})
-
- } catch (error) {
- console.log(error);
- res.json({success:false,message:"Error"})
- }
-}
+import userModel from "../models/userModel.js";
+import restaurantModel from "../models/restaurantModel.js";
+import Stripe from "stripe";
+import { filterForOwner } from "../middleware/access.js";
+
+const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
+const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:5174";
+
+const DELIVERY_FEE_DRIVER = 20000;
+const DELIVERY_FEE_DRONE = 30000;
+
+const buildTotals = (items = [], deliveryType, overrides = {}) => {
+ const subTotal = overrides.subTotal ??
+ items.reduce((sum, it) => sum + Number(it.price || 0) * Number(it.quantity || 1), 0);
+ const deliveryFee =
+ overrides.deliveryFee ??
+ (deliveryType === "drone" ? DELIVERY_FEE_DRONE : DELIVERY_FEE_DRIVER);
+ const promoDiscount = overrides.promoDiscount ?? 0;
+ const total = overrides.total ?? Math.max(0, subTotal + deliveryFee - promoDiscount);
+
+ return { subTotal, deliveryFee, promoDiscount, total, amount: total };
+};
+
+const buildSubOrders = (items = [], deliveryType, deliveryFee, etaMinutes) => {
+ const byRestaurant = items.reduce((map, it) => {
+ const rId = it.restaurantId?.toString();
+ if (!rId) return map;
+ if (!map[rId]) map[rId] = [];
+ map[rId].push(it);
+ return map;
+ }, {});
+
+ return Object.entries(byRestaurant).map(([restaurantId, list]) => ({
+ restaurantId,
+ items: list,
+ deliveryType,
+ deliveryFee,
+ etaMinutes,
+ }));
+};
+
+// placing user order from frontend (Stripe Checkout)
+const placeOrder = async (req, res) => {
+ try {
+ const {
+ userId,
+ items = [],
+ address,
+ deliveryType = "driver",
+ promoCode,
+ promoDiscount = 0,
+ etaMinutes,
+ route = [],
+ } = req.body;
+
+ const totals = buildTotals(items, deliveryType, {
+ promoDiscount,
+ subTotal: req.body.subTotal,
+ deliveryFee: req.body.deliveryFee,
+ total: req.body.total,
+ amount: req.body.amount,
+ });
+
+ // Kiểm tra nhà hàng active cho tất cả items
+ const uniqueRestaurants = Array.from(
+ new Set((items || []).map((it) => it.restaurantId?.toString()).filter(Boolean))
+ );
+ if (uniqueRestaurants.length) {
+ const inactive = await restaurantModel.find({
+ _id: { $in: uniqueRestaurants },
+ active: false,
+ });
+ if (inactive.length) {
+ return res.status(400).json({ success: false, message: "Restaurant inactive, cannot place order" });
+ }
+ }
+
+ const newOrder = new orderModel({
+ userId,
+ items,
+ amount: totals.amount,
+ address,
+ deliveryType,
+ deliveryFee: totals.deliveryFee,
+ etaMinutes,
+ promoCode,
+ promoDiscount: totals.promoDiscount,
+ subTotal: totals.subTotal,
+ total: totals.total,
+ payment: true,
+ paymentMethod: "card",
+ route,
+ subOrders: buildSubOrders(items, deliveryType, totals.deliveryFee, etaMinutes),
+ statusTimeline: [{ status: "Food Processing", at: new Date() }],
+ });
+ await newOrder.save();
+
+ // clear cart on server immediately (kept same as original logic)
+ await userModel.findByIdAndUpdate(userId, { cartData: {} });
+
+ // Stripe amounts must be in minor units; choose USD for test mode (VND không được hỗ trợ trực tiếp)
+ const toUsdCents = (vnd) => Math.max(1, Math.round((Number(vnd) || 0) / 24000 * 100));
+
+ const line_items = items.map((item) => ({
+ price_data: {
+ currency: "usd",
+ product_data: { name: item.name },
+ unit_amount: toUsdCents(item.price),
+ },
+ quantity: item.quantity,
+ }));
+
+ line_items.push({
+ price_data: {
+ currency: "usd",
+ product_data: { name: "Delivery Charges" },
+ unit_amount: toUsdCents(totals.deliveryFee),
+ },
+ quantity: 1,
+ });
+
+ const session = await stripe.checkout.sessions.create({
+ line_items,
+ mode: "payment",
+ success_url: `${FRONTEND_URL}/verify?success=true&orderId=${newOrder._id}`,
+ cancel_url: `${FRONTEND_URL}/verify?success=false&orderId=${newOrder._id}`,
+ client_reference_id: newOrder._id.toString(),
+ metadata: {
+ orderId: newOrder._id.toString(),
+ userId: userId?.toString() || "",
+ },
+ });
+
+ res.json({ success: true, session_url: session.url, orderId: newOrder._id });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+// COD: place order without Stripe
+const placeOrderCOD = async (req, res) => {
+ try {
+ const {
+ userId,
+ items = [],
+ address,
+ deliveryType = "driver",
+ promoCode,
+ promoDiscount = 0,
+ etaMinutes,
+ route = [],
+ } = req.body;
+
+ const totals = buildTotals(items, deliveryType, {
+ promoDiscount,
+ subTotal: req.body.subTotal,
+ deliveryFee: req.body.deliveryFee,
+ total: req.body.total,
+ amount: req.body.amount,
+ });
+
+ // Kiểm tra nhà hàng active
+ const uniqueRestaurants = Array.from(
+ new Set((items || []).map((it) => it.restaurantId?.toString()).filter(Boolean))
+ );
+ if (uniqueRestaurants.length) {
+ const inactive = await restaurantModel.find({
+ _id: { $in: uniqueRestaurants },
+ active: false,
+ });
+ if (inactive.length) {
+ return res.status(400).json({ success: false, message: "Restaurant inactive, cannot place order" });
+ }
+ }
+
+ const newOrder = new orderModel({
+ userId,
+ items,
+ amount: totals.amount,
+ address,
+ payment: false, // unpaid, COD
+ paymentMethod: "cod",
+ deliveryType,
+ deliveryFee: totals.deliveryFee,
+ etaMinutes,
+ promoCode,
+ promoDiscount: totals.promoDiscount,
+ subTotal: totals.subTotal,
+ total: totals.total,
+ route,
+ subOrders: buildSubOrders(items, deliveryType, totals.deliveryFee, etaMinutes),
+ statusTimeline: [{ status: "Food Processing", at: new Date() }],
+ });
+ await newOrder.save();
+
+ // clear cart on server
+ await userModel.findByIdAndUpdate(userId, { cartData: {} });
+
+ res.json({ success: true, orderId: newOrder._id });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+const verifyOrder = async (req, res) => {
+ const { orderId, success } = req.body;
+ try {
+ if (success == "true") {
+ await orderModel.findByIdAndUpdate(orderId, {
+ payment: true,
+ $push: { statusTimeline: { status: "Paid", at: new Date() } },
+ });
+ res.json({ success: true, message: "Paid" });
+ } else {
+ await orderModel.findByIdAndDelete(orderId);
+ res.json({ success: false, message: "Not Paid" });
+ }
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+// user orders for frontend (sorted newest-first)
+const userOrders = async (req, res) => {
+ try {
+ const orders = await orderModel
+ .find({ userId: req.body.userId })
+ .sort({ date: -1 });
+ res.json({ success: true, data: orders });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+// Listing orders for admin panel (sorted newest-first)
+const listOrders = async (req, res) => {
+ try {
+ const baseFilter = {};
+ const requestedRestaurantIds = [];
+ if (req.query.restaurantId) {
+ baseFilter["items.restaurantId"] = req.query.restaurantId;
+ requestedRestaurantIds.push(req.query.restaurantId.toString());
+ }
+ const filter = filterForOwner(req.user, baseFilter, "items.restaurantId");
+ if (req.user?.role === "restaurantOwner" && Array.isArray(req.user.restaurantIds)) {
+ requestedRestaurantIds.push(...req.user.restaurantIds.map((id) => id.toString()));
+ }
+ const requestedSet = new Set(requestedRestaurantIds);
+
+ const orders = await orderModel.find(filter).sort({ date: -1 });
-const verifyOrder = async (req,res) => {
- const {orderId,success} = req.body;
- try {
- if (success=="true") {
- await orderModel.findByIdAndUpdate(orderId,{payment:true});
- res.json({success:true,message:"Paid"})
+ // Nếu lọc theo nhà hàng, chỉ trả items/subOrders thuộc nhà hàng đó
+ let data = orders;
+ if (requestedSet.size) {
+ data = orders.map((ord) => {
+ const o = ord.toObject();
+ const allRestIds = Array.from(new Set((o.items || []).map((it) => it.restaurantId?.toString()).filter(Boolean)));
+
+ o.items = o.items?.filter((it) => requestedSet.has(it.restaurantId?.toString()));
+ if (o.subOrders?.length) {
+ o.subOrders = o.subOrders.filter((sub) => requestedSet.has(sub.restaurantId?.toString()));
}
- else{
- await orderModel.findByIdAndDelete(orderId);
- res.json({success:false,message:"Not Paid"})
+ // Recalculate totals for the filtered items (so each restaurant sees its own totals)
+ const subTotalFiltered =
+ o.items?.reduce((sum, it) => sum + Number(it.price || 0) * Number(it.quantity || 1), 0) || 0;
+
+ let deliveryFiltered =
+ o.subOrders?.reduce((sum, sub) => sum + Number(sub.deliveryFee || 0), 0) || 0;
+ // fallback: nếu subOrders rỗng, chia đều phí giao theo số nhà hàng trong đơn gốc
+ if (!deliveryFiltered && allRestIds.length > 0) {
+ deliveryFiltered = (Number(o.deliveryFee || 0) || 0) / allRestIds.length;
}
- } catch (error) {
- console.log(error);
- res.json({success:false,message:"Error"})
+
+ o.subTotal = subTotalFiltered;
+ o.deliveryFee = deliveryFiltered;
+ o.promoDiscount = 0; // không phân bổ giảm giá theo nhà hàng, tránh hiển thị sai lệch
+ o.total = subTotalFiltered + deliveryFiltered;
+ o.amount = o.total;
+ return o;
+ });
}
-}
+ res.json({ success: true, data });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+// api for updating order status
+const updateStatus = async (req, res) => {
+ try {
+ const orderId = req.body.orderId;
+ const newStatus = req.body.status;
+ let restaurantId = req.body.restaurantId;
+ if (!orderId || !newStatus) return res.status(400).json({ success: false, message: "orderId and status required" });
+
+ const order = await orderModel.findById(orderId);
+ if (!order) return res.status(404).json({ success: false, message: "Order not found" });
+ const normalizedStatus = typeof newStatus === "string" ? newStatus.toLowerCase() : "";
+ const setPayment = normalizedStatus === "delivered" && order.paymentMethod === "cod";
+ // map status -> deliveryPhase
+ const phaseMap = {
+ "Food Processing": "at_restaurant",
+ "Out For Delivery": "delivering",
+ Delivered: "delivered",
+ };
+ const phaseUpdate = phaseMap[newStatus] || undefined;
+
+ // Owner can only update orders from their restaurants
+ if (req.user?.role === "restaurantOwner") {
+ const allowed = (req.user.restaurantIds || []).map((id) => id.toString());
+ const restInOrder = Array.from(
+ new Set([
+ ...(order.subOrders || []).map((s) => s.restaurantId?.toString()).filter(Boolean),
+ ...(order.items || []).map((it) => it.restaurantId?.toString()).filter(Boolean),
+ ])
+ );
+
+ if (!restaurantId) {
+ // nếu owner chỉ có một nhà hàng trong đơn, tự chọn
+ const intersection = restInOrder.filter((r) => allowed.includes(r));
+ if (intersection.length === 1) {
+ restaurantId = intersection[0];
+ } else {
+ return res
+ .status(400)
+ .json({ success: false, message: "restaurantId required for this order" });
+ }
+ }
+
+ if (!allowed.includes(String(restaurantId))) {
+ return res.status(403).json({ success: false, message: "Not allowed for this restaurant" });
+ }
+ if (!restInOrder.includes(String(restaurantId))) {
+ return res.status(404).json({ success: false, message: "Restaurant not in this order" });
+ }
+
+ // Cập nhật subOrder nếu có, nếu không thì cập nhật toàn bộ đơn (trường hợp đơn 1 nhà hàng không có subOrders)
+ const updateDoc =
+ order.subOrders?.length
+ ? {
+ $set: {
+ "subOrders.$[s].status": newStatus,
+ status: newStatus,
+ ...(setPayment ? { payment: true } : {}),
+ ...(phaseUpdate ? { deliveryPhase: phaseUpdate } : {}),
+ },
+ $push: { statusTimeline: { status: newStatus, at: new Date(), restaurantId } },
+ }
+ : {
+ status: newStatus,
+ ...(setPayment ? { payment: true } : {}),
+ ...(phaseUpdate ? { deliveryPhase: phaseUpdate } : {}),
+ $push: { statusTimeline: { status: newStatus, at: new Date(), restaurantId } },
+ };
+
+ const updated = await orderModel.findOneAndUpdate(
+ { _id: orderId },
+ updateDoc,
+ {
+ arrayFilters: order.subOrders?.length ? [{ "s.restaurantId": restaurantId }] : undefined,
+ new: true,
+ }
+ );
-// user orders for frontend
+ if (!updated) {
+ return res.status(404).json({ success: false, message: "Order or sub order not found" });
+ }
-const userOrders = async (req,res) => {
- try {
- const orders = await orderModel.find({userId:req.body.userId});
- res.json({success:true,data:orders})
- } catch (error) {
- console.log(error);
- res.json({success:false,message:"Error"})
+ return res.json({ success: true, message: "Status Updated (restaurant scope)" });
}
-}
-// Listing orders for admin panel
-const listOrders = async (req,res) => {
- try {
- const orders = await orderModel.find({});
- res.json({success:true,data:orders})
- } catch (error) {
- console.log(error);
- res.json({success:false,message:"Error"})
+ // Admin: nếu có restaurantId và có subOrders, chỉ cập nhật subOrder đó; ngược lại cập nhật toàn đơn
+ if (restaurantId && order.subOrders?.length) {
+ const updated = await orderModel.findOneAndUpdate(
+ { _id: orderId },
+ {
+ $set: {
+ "subOrders.$[s].status": newStatus,
+ status: newStatus,
+ ...(setPayment ? { payment: true } : {}),
+ ...(phaseUpdate ? { deliveryPhase: phaseUpdate } : {}),
+ },
+ $push: { statusTimeline: { status: newStatus, at: new Date(), restaurantId } },
+ },
+ {
+ arrayFilters: [{ "s.restaurantId": restaurantId }],
+ new: true,
+ }
+ );
+ if (!updated) {
+ return res.status(404).json({ success: false, message: "Sub order not found for restaurant" });
+ }
+ return res.json({ success: true, message: "Status Updated (restaurant scope)" });
}
-}
-// api for updating order status
-const updateStatus = async (req,res) => {
- try {
- await orderModel.findByIdAndUpdate(req.body.orderId,{status:req.body.status});
- res.json({success:true,message:"Status Updated"})
- } catch (error) {
- console.log(error);
- res.json({success:false,message:"Error"})
+ await orderModel.findByIdAndUpdate(orderId, {
+ status: newStatus,
+ ...(setPayment ? { payment: true } : {}),
+ ...(phaseUpdate ? { deliveryPhase: phaseUpdate } : {}),
+ $push: { statusTimeline: { status: newStatus, at: new Date() } },
+ });
+ res.json({ success: true, message: "Status Updated" });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+// admin/owner updates delivery phase, droneId, driverId
+const updateDeliveryPhase = async (req, res) => {
+ try {
+ const { orderId } = req.params;
+ const { phase, droneId, driverId } = req.body || {};
+ const allowedPhases = ["at_restaurant", "delivering", "delivered"];
+ if (phase && !allowedPhases.includes(phase)) {
+ return res.status(400).json({ success: false, message: "Invalid phase" });
}
-}
+ const order = await orderModel.findById(orderId);
+ if (!order) return res.status(404).json({ success: false, message: "Order not found" });
-export {placeOrder,verifyOrder,userOrders,listOrders,updateStatus}
\ No newline at end of file
+ // restaurantOwner must belong to the restaurants in the order
+ if (req.user?.role === "restaurantOwner") {
+ const allowed = (req.user.restaurantIds || []).map((id) => id.toString());
+ const restInOrder = Array.from(
+ new Set(
+ [
+ ...(order.subOrders || []).map((s) => s.restaurantId?.toString()).filter(Boolean),
+ ...(order.items || []).map((it) => it.restaurantId?.toString()).filter(Boolean),
+ ].filter(Boolean)
+ )
+ );
+ const hasPermission = restInOrder.some((r) => allowed.includes(r));
+ if (!hasPermission) {
+ return res.status(403).json({ success: false, message: "Not allowed for this order" });
+ }
+ }
+
+ if (phase) order.deliveryPhase = phase;
+ if (typeof droneId !== "undefined") order.droneId = droneId;
+ if (typeof driverId !== "undefined") order.driverId = driverId;
+
+ // sync status with phase if provided
+ if (phase) {
+ const statusMap = {
+ at_restaurant: "Food Processing",
+ delivering: "Out For Delivery",
+ delivered: "Delivered",
+ };
+ const mapped = statusMap[phase];
+ if (mapped) {
+ order.status = mapped;
+ order.statusTimeline = order.statusTimeline || [];
+ order.statusTimeline.push({ status: mapped, at: new Date() });
+ if (phase === "delivered" && order.paymentMethod === "cod") {
+ order.payment = true;
+ }
+ }
+ }
+
+ const saved = await order.save();
+ res.json({ success: true, data: saved });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+export {
+ placeOrder,
+ placeOrderCOD,
+ verifyOrder,
+ userOrders,
+ listOrders,
+ updateStatus,
+ updateDeliveryPhase,
+}
diff --git a/backend/controllers/promotionController.js b/backend/controllers/promotionController.js
new file mode 100644
index 0000000..1602a82
--- /dev/null
+++ b/backend/controllers/promotionController.js
@@ -0,0 +1,111 @@
+import promotionModel from "../models/promotionModel.js";
+import { filterForOwner } from "../middleware/access.js";
+
+const now = () => new Date();
+const t = (lang, vi, en) => (lang === "vi" ? vi : en);
+
+const applyPromotion = async (req, res) => {
+ try {
+ const { code, subTotal, restaurantId, cityId, lang = "en" } = req.body;
+ const normalizedCode = typeof code === "string" ? code.trim().toUpperCase() : "";
+ const subTotalNum = Number(subTotal);
+ if (!normalizedCode || Number.isNaN(subTotalNum)) {
+ return res
+ .status(400)
+ .json({ success: false, message: t(lang, "Thiếu mã hoặc subTotal", "Missing code or subTotal") });
+ }
+
+ const promo = await promotionModel.findOne({ code: normalizedCode, active: true });
+ if (!promo) return res.json({ success: false, message: t(lang, "Mã không hợp lệ", "Invalid code") });
+
+ const nowDate = now();
+ if ((promo.startAt && nowDate < promo.startAt) || (promo.endAt && nowDate > promo.endAt)) {
+ return res.json({
+ success: false,
+ message: t(lang, "Mã hết hạn hoặc chưa bắt đầu", "Promotion expired or not started"),
+ });
+ }
+ if (promo.cityId && cityId && promo.cityId.toString() !== cityId.toString()) {
+ return res.json({
+ success: false,
+ message: t(lang, "Mã không áp dụng cho thành phố này", "Code not valid for this city"),
+ });
+ }
+ if (promo.restaurantId && restaurantId && promo.restaurantId.toString() !== restaurantId.toString()) {
+ return res.json({
+ success: false,
+ message: t(lang, "Mã không áp dụng cho nhà hàng này", "Code not valid for this restaurant"),
+ });
+ }
+ if (promo.minOrder && subTotalNum < promo.minOrder) {
+ return res.json({
+ success: false,
+ message: t(lang, `Cần tối thiểu ${promo.minOrder}`, `Min order ${promo.minOrder}`),
+ });
+ }
+
+ let discount = 0;
+ if (promo.type === "percent") {
+ discount = (subTotalNum * promo.value) / 100;
+ } else if (promo.type === "amount") {
+ discount = promo.value;
+ }
+ if (promo.maxDiscount) {
+ discount = Math.min(discount, promo.maxDiscount);
+ }
+ discount = Math.max(0, Math.min(discount, subTotalNum));
+
+ return res.json({
+ success: true,
+ discount,
+ promo,
+ message: t(lang, "Áp dụng mã thành công", "Promotion applied"),
+ });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({
+ success: false,
+ message: t(req.body.lang || "en", "Lỗi hệ thống", "Error"),
+ });
+ }
+};
+
+const createPromotion = async (req, res) => {
+ try {
+ if (req.user?.role === "restaurantOwner") {
+ const allowed = req.user.restaurantIds || [];
+ if (!req.body.restaurantId) {
+ return res.status(400).json({ success: false, message: "restaurantId required for owner" });
+ }
+ if (!allowed.find((id) => id.toString() === req.body.restaurantId.toString())) {
+ return res.status(403).json({ success: false, message: "Not allowed for this restaurant" });
+ }
+ }
+
+ const payload = {
+ ...req.body,
+ code: req.body.code?.toUpperCase(),
+ };
+ const promo = await promotionModel.create(payload);
+ return res.status(201).json({ success: true, data: promo });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+const listPromotions = async (req, res) => {
+ try {
+ const baseFilter = {};
+ if (req.query.restaurantId) baseFilter.restaurantId = req.query.restaurantId;
+ if (req.query.cityId) baseFilter.cityId = req.query.cityId;
+ const filter = filterForOwner(req.user, baseFilter);
+ const promos = await promotionModel.find(filter).sort({ createdAt: -1 });
+ return res.json({ success: true, data: promos });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+export { applyPromotion, createPromotion, listPromotions };
diff --git a/backend/controllers/restaurantController.js b/backend/controllers/restaurantController.js
new file mode 100644
index 0000000..827227c
--- /dev/null
+++ b/backend/controllers/restaurantController.js
@@ -0,0 +1,98 @@
+import restaurantModel from "../models/restaurantModel.js";
+import { filterForOwner } from "../middleware/access.js";
+
+const addRestaurant = async (req, res) => {
+ try {
+ const {
+ name,
+ nameEn,
+ slug,
+ cityId,
+ address,
+ addressEn,
+ coords,
+ image,
+ deliveryModes,
+ minOrder,
+ active = true,
+ } = req.body;
+
+ if (!name || !slug || !cityId) {
+ return res.status(400).json({ success: false, message: "Missing required fields" });
+ }
+
+ const restaurant = await restaurantModel.create({
+ name,
+ nameEn,
+ slug,
+ cityId,
+ address,
+ addressEn,
+ coords,
+ image,
+ deliveryModes,
+ minOrder,
+ active,
+ });
+
+ return res.status(201).json({ success: true, data: restaurant });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+const listRestaurants = async (req, res) => {
+ try {
+ const baseFilter = {};
+ if (req.query.cityId) baseFilter.cityId = req.query.cityId;
+
+ const isAdmin = req.user?.role === "admin";
+ const activeParam = req.query.active;
+ if (typeof activeParam !== "undefined") {
+ if (String(activeParam).toLowerCase() !== "all") {
+ baseFilter.active = String(activeParam).toLowerCase() === "true";
+ }
+ } else if (!isAdmin) {
+ // mặc định: người dùng/owner chỉ thấy active=true
+ baseFilter.active = true;
+ }
+
+ const filter = filterForOwner(req.user, baseFilter, "_id");
+ const restaurants = await restaurantModel.find(filter).sort({ name: 1 });
+ return res.json({ success: true, data: restaurants });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+const toggleRestaurant = async (req, res) => {
+ try {
+ const { id, active } = req.body;
+ if (!id) return res.status(400).json({ success: false, message: "Missing id" });
+
+ if (req.user?.role === "restaurantOwner") {
+ const allowed = req.user.restaurantIds || [];
+ if (!allowed.find((rid) => rid.toString() === id.toString())) {
+ return res.status(403).json({ success: false, message: "Not allowed for this restaurant" });
+ }
+ }
+
+ // Coerce active to boolean
+ const activeFlag =
+ typeof active === "boolean"
+ ? active
+ : typeof active === "string"
+ ? active.toLowerCase() === "true"
+ : false;
+
+ await restaurantModel.findByIdAndUpdate(id, { active: activeFlag });
+ return res.json({ success: true });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+export { addRestaurant, listRestaurants, toggleRestaurant };
diff --git a/backend/controllers/reviewController.js b/backend/controllers/reviewController.js
new file mode 100644
index 0000000..c92cba8
--- /dev/null
+++ b/backend/controllers/reviewController.js
@@ -0,0 +1,83 @@
+import reviewModel from "../models/reviewModel.js";
+import orderModel from "../models/orderModel.js";
+
+// User tạo review sau khi đơn hoàn tất
+const addReview = async (req, res) => {
+ try {
+ const { orderId, userId, restaurantId, rating, ratingFood, ratingDriver, comment, images } = req.body;
+ if (!orderId || !userId || !restaurantId) {
+ return res.status(400).json({ success: false, message: "Missing required fields" });
+ }
+
+ const order = await orderModel.findById(orderId);
+ if (!order || order.userId.toString() !== userId.toString()) {
+ return res.status(403).json({ success: false, message: "Not allowed" });
+ }
+
+ const food = Number(ratingFood);
+ const driver = Number(ratingDriver);
+ const overall = Number(rating);
+
+ // dùng các số hợp lệ để tính trung bình (ưu tiên food/driver, fallback overall)
+ const nums = [];
+ if (!Number.isNaN(food) && food > 0) nums.push(food);
+ if (!Number.isNaN(driver) && driver > 0) nums.push(driver);
+ if (!nums.length && !Number.isNaN(overall) && overall > 0) nums.push(overall);
+
+ const ratingAvg = nums.length
+ ? Math.min(5, Math.max(1, Math.round((nums.reduce((a, b) => a + b, 0) / nums.length) * 10) / 10))
+ : undefined;
+
+ const ratingFoodFinal = !Number.isNaN(food) && food > 0
+ ? food
+ : (!Number.isNaN(overall) && overall > 0 ? overall : undefined);
+ const ratingDriverFinal = !Number.isNaN(driver) && driver > 0 ? driver : undefined;
+ const ratingFinal = ratingAvg ?? ratingFoodFinal ?? ratingDriverFinal ?? (!Number.isNaN(overall) && overall > 0 ? overall : undefined);
+
+ const review = await reviewModel.create({
+ orderId,
+ userId,
+ restaurantId,
+ rating: ratingFinal,
+ ratingFood: ratingFoodFinal,
+ ratingDriver: ratingDriverFinal,
+ ratingAvg,
+ comment,
+ images: Array.isArray(images) ? images : [],
+ });
+
+ return res.status(201).json({ success: true, data: review });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+const listReviews = async (req, res) => {
+ try {
+ const filter = {};
+ if (req.query.restaurantId) filter.restaurantId = req.query.restaurantId;
+ if (req.query.orderId) filter.orderId = req.query.orderId;
+ if (req.query.userId) filter.userId = req.query.userId;
+ if (req.query.status) filter.status = req.query.status;
+
+ const reviews = await reviewModel.find(filter).sort({ createdAt: -1 });
+ return res.json({ success: true, data: reviews });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+const approveReview = async (req, res) => {
+ try {
+ const { id, status = "approved" } = req.body;
+ await reviewModel.findByIdAndUpdate(id, { status });
+ return res.json({ success: true });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+export { addReview, listReviews, approveReview };
diff --git a/backend/controllers/supportController.js b/backend/controllers/supportController.js
new file mode 100644
index 0000000..13ff7a9
--- /dev/null
+++ b/backend/controllers/supportController.js
@@ -0,0 +1,65 @@
+import supportTicketModel from "../models/supportTicketModel.js";
+
+const createTicket = async (req, res) => {
+ try {
+ const { userId, orderId, subject, message } = req.body;
+ if (!userId || !subject || !message) {
+ return res.status(400).json({ success: false, message: "Missing required fields" });
+ }
+ const ticket = await supportTicketModel.create({
+ userId,
+ orderId,
+ subject,
+ message,
+ });
+ return res.status(201).json({ success: true, data: ticket });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+const listTickets = async (req, res) => {
+ try {
+ const filter = {};
+ if (req.query.status) filter.status = req.query.status;
+ if (req.query.userId) filter.userId = req.query.userId;
+ const tickets = await supportTicketModel.find(filter).sort({ createdAt: -1 });
+ return res.json({ success: true, data: tickets });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+const replyTicket = async (req, res) => {
+ try {
+ const { id, by, message } = req.body;
+ if (!id || !by || !message) {
+ return res.status(400).json({ success: false, message: "Missing fields" });
+ }
+ await supportTicketModel.findByIdAndUpdate(id, {
+ $push: { replies: { by, message, at: new Date() } },
+ });
+ return res.json({ success: true });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+const updateTicketStatus = async (req, res) => {
+ try {
+ const { id, status } = req.body;
+ if (!id || !status) {
+ return res.status(400).json({ success: false, message: "Missing id/status" });
+ }
+ await supportTicketModel.findByIdAndUpdate(id, { status });
+ return res.json({ success: true });
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ success: false, message: "Error" });
+ }
+};
+
+export { createTicket, listTickets, replyTicket, updateTicketStatus };
diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js
index 20f0313..05b817e 100644
--- a/backend/controllers/userController.js
+++ b/backend/controllers/userController.js
@@ -2,6 +2,7 @@ import userModel from "../models/userModel.js";
import jwt from "jsonwebtoken"
import bcrypt from "bcrypt"
import validator from "validator"
+import restaurantModel from "../models/restaurantModel.js"
// login user
const loginUser = async (req,res) => {
@@ -13,22 +14,40 @@ const loginUser = async (req,res) => {
return res.json({success:false,message:"User doesn't exist."})
}
+ if (user.active === false) {
+ return res.json({success:false,message:"Account is inactive"})
+ }
+
const isMatch = await bcrypt.compare(password,user.password);
if (!isMatch) {
return res.json({success:false,message:"Invalid credentials"})
}
- const token = createToken(user._id);
- res.json({success:true,token})
+ const token = createToken(user);
+ res.json({
+ success: true,
+ token,
+ role: user.role,
+ restaurantIds: user.restaurantIds || [],
+ name: user.name || "",
+ message: "Login success",
+ })
} catch (error) {
console.log(error);
res.json({success:false,message:"Error"})
}
}
-const createToken = (id) => {
- return jwt.sign({id},process.env.JWT_SECRET)
+const createToken = (user) => {
+ return jwt.sign(
+ {
+ id: user._id,
+ role: user.role || "user",
+ restaurantIds: user.restaurantIds || [],
+ },
+ process.env.JWT_SECRET
+ );
}
// register user
@@ -62,12 +81,77 @@ const registerUser = async (req,res) => {
})
const user = await newUser.save()
- const token = createToken(user._id)
- res.json({success:true,token});
+ const token = createToken(user)
+ res.json({
+ success: true,
+ token,
+ role: user.role,
+ restaurantIds: user.restaurantIds || [],
+ name: user.name || "",
+ });
} catch (error) {
console.log(error);
res.json({success:false,message:"Error"})
}
}
-export {loginUser,registerUser}
\ No newline at end of file
+export {loginUser,registerUser}
+
+// ---- Admin user management (simple) ----
+export const listUsers = async (req, res) => {
+ try {
+ const { q, active, role } = req.query;
+ const filter = {};
+ if (typeof active !== "undefined") filter.active = active === "true";
+ if (role) filter.role = role;
+ if (q) {
+ filter.$or = [
+ { name: { $regex: q, $options: "i" } },
+ { email: { $regex: q, $options: "i" } },
+ ];
+ }
+ const users = await userModel
+ .find(filter, { password: 0 })
+ .sort({ createdAt: -1 });
+ res.json({ success: true, data: users });
+ } catch (error) {
+ console.log(error);
+ res.json({ success: false, message: "Error" });
+ }
+};
+
+export const updateUser = async (req, res) => {
+ try {
+ const { userId, role, active } = req.body;
+ const update = {};
+ const willChangeActive = typeof active === "boolean";
+ if (role) update.role = role;
+ if (willChangeActive) update.active = active;
+
+ const user = await userModel.findByIdAndUpdate(userId, update, { new: true });
+
+ // Cascade toggle restaurants if owner
+ if (user && user.role === "restaurantOwner" && willChangeActive) {
+ const ids = user.restaurantIds || [];
+ if (ids.length) {
+ await restaurantModel.updateMany({ _id: { $in: ids } }, { active });
+ }
+ }
+
+ res.json({ success: true, message: "Updated" });
+ } catch (error) {
+ console.log(error);
+ res.json({ success: false, message: "Error" });
+ }
+};
+
+export const removeUser = async (req, res) => {
+ try {
+ const { userId } = req.body;
+ await userModel.findByIdAndDelete(userId);
+ res.json({ success: true, message: "Removed" });
+ } catch (error) {
+ console.log(error);
+ res.json({ success: false, message: "Error" });
+ }
+};
diff --git a/backend/middleware/access.js b/backend/middleware/access.js
new file mode 100644
index 0000000..0e77b14
--- /dev/null
+++ b/backend/middleware/access.js
@@ -0,0 +1,30 @@
+export const requireOwnerOrAdmin = (req, res, next) => {
+ const user = req.user;
+ if (!user) return res.status(401).json({ success: false, message: "Not authorized" });
+ if (user.role === "admin") return next();
+
+ const allowed = user.restaurantIds || [];
+ const restaurantId =
+ req.body.restaurantId ||
+ req.query.restaurantId ||
+ req.params.restaurantId ||
+ req.body.id ||
+ req.params.id ||
+ req.query.id;
+
+ if (user.role === "restaurantOwner") {
+ if (!restaurantId || allowed.find((id) => id.toString() === restaurantId.toString())) {
+ return next();
+ }
+ return res.status(403).json({ success: false, message: "Not allowed for this restaurant" });
+ }
+ return res.status(403).json({ success: false, message: "Not allowed" });
+};
+
+export const filterForOwner = (user, filter = {}, field = "restaurantId") => {
+ if (user?.role === "restaurantOwner") {
+ const allowed = user.restaurantIds || [];
+ return { ...filter, [field]: { $in: allowed } };
+ }
+ return filter;
+};
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
index 6222427..2a74478 100644
--- a/backend/middleware/auth.js
+++ b/backend/middleware/auth.js
@@ -1,18 +1,58 @@
-import jwt from "jsonwebtoken"
+import jwt from "jsonwebtoken";
+import userModel from "../models/userModel.js";
-const authMiddleware = async (req,res,next) => {
- const {token} = req.headers;
- if (!token){
- return res.json({success:false,message:"Not Authorized Login Again"})
- }
- try {
- const token_decode = jwt.verify(token,process.env.JWT_SECRET);
- req.body.userId = token_decode.id;
- next();
- } catch (error) {
- console.log(error);
- res.json({success:false,message:"Error"})
+const getToken = (req) => {
+ const bearer = req.headers.authorization || req.headers.Authorization;
+ if (typeof bearer === "string" && bearer.toLowerCase().startsWith("bearer ")) {
+ return bearer.slice(7).trim();
+ }
+ return req.headers.token;
+};
+
+const decodeToken = (token) => jwt.verify(token, process.env.JWT_SECRET);
+
+const authMiddleware = async (req, res, next) => {
+ const token = getToken(req);
+ if (!token) {
+ return res.status(401).json({ success: false, message: "Not Authorized Login Again" });
+ }
+ try {
+ const token_decode = decodeToken(token);
+
+ // Kiểm tra trạng thái user trong DB (chặn token cũ khi bị khóa)
+ const dbUser = await userModel.findById(token_decode.id);
+ if (!dbUser || dbUser.active === false) {
+ return res.status(403).json({ success: false, message: "Account is inactive" });
}
-}
-export default authMiddleware;
\ No newline at end of file
+ req.body.userId = token_decode.id;
+ req.user = {
+ id: token_decode.id,
+ role: token_decode.role || dbUser.role || "user",
+ restaurantIds: token_decode.restaurantIds || dbUser.restaurantIds || [],
+ };
+ next();
+ } catch (error) {
+ console.log(error);
+ res.status(401).json({ success: false, message: "Error" });
+ }
+};
+
+// Optional auth: attach req.user if token present, otherwise continue
+export const optionalAuth = (req, _res, next) => {
+ const token = getToken(req);
+ if (!token) return next();
+ try {
+ const token_decode = decodeToken(token);
+ req.user = {
+ id: token_decode.id,
+ role: token_decode.role || "user",
+ restaurantIds: token_decode.restaurantIds || [],
+ };
+ } catch (err) {
+ console.log("Optional auth parse failed", err?.message);
+ }
+ next();
+};
+
+export default authMiddleware;
diff --git a/backend/models/cityModel.js b/backend/models/cityModel.js
new file mode 100644
index 0000000..72c3d38
--- /dev/null
+++ b/backend/models/cityModel.js
@@ -0,0 +1,14 @@
+import mongoose from "mongoose";
+
+const citySchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true },
+ nameEn: { type: String },
+ code: { type: String, required: true, unique: true },
+ active: { type: Boolean, default: true },
+ },
+ { timestamps: true }
+);
+
+const cityModel = mongoose.models.city || mongoose.model("city", citySchema);
+export default cityModel;
diff --git a/backend/models/deliveryPartnerModel.js b/backend/models/deliveryPartnerModel.js
new file mode 100644
index 0000000..347896d
--- /dev/null
+++ b/backend/models/deliveryPartnerModel.js
@@ -0,0 +1,21 @@
+import mongoose from "mongoose";
+
+const deliveryPartnerSchema = new mongoose.Schema(
+ {
+ type: { type: String, enum: ["drone", "driver"], required: true },
+ status: { type: String, enum: ["idle", "busy", "offline"], default: "idle" },
+ speedKmH: { type: Number, default: 40 }, // drone có thể 60+
+ capacity: { type: Number, default: 1 },
+ currentCoords: {
+ lat: { type: Number },
+ lng: { type: Number },
+ },
+ name: { type: String },
+ },
+ { timestamps: true }
+);
+
+const deliveryPartnerModel =
+ mongoose.models.deliveryPartner ||
+ mongoose.model("deliveryPartner", deliveryPartnerSchema);
+export default deliveryPartnerModel;
diff --git a/backend/models/foodModel.js b/backend/models/foodModel.js
index c546194..d9ea856 100644
--- a/backend/models/foodModel.js
+++ b/backend/models/foodModel.js
@@ -1,13 +1,22 @@
import mongoose from "mongoose";
-const foodSchema = new mongoose.Schema({
- name: {type:String,required:true},
- description: {type:String,required:true},
- price:{type:Number,required:true},
- image:{type:String,required:true},
- category:{type:String,required:true}
-})
+const foodSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true }, // tiếng Việt mặc định
+ nameEn: { type: String }, // tên tiếng Anh (tùy chọn)
+ description: { type: String, required: true }, // mô tả tiếng Anh hoặc chung
+ descriptionVi: { type: String }, // mô tả tiếng Việt (tùy chọn)
+ price: { type: Number, required: true },
+ image: { type: String, required: true },
+ category: { type: String, required: true },
+ restaurantId: { type: mongoose.Schema.Types.ObjectId, ref: "restaurant" },
+ cityId: { type: mongoose.Schema.Types.ObjectId, ref: "city" },
+ tags: [{ type: String }],
+ active: { type: Boolean, default: true },
+ },
+ { timestamps: true }
+);
-const foodModel = mongoose.models.food || mongoose.model("food",foodSchema)
+const foodModel = mongoose.models.food || mongoose.model("food", foodSchema);
-export default foodModel;
\ No newline at end of file
+export default foodModel;
diff --git a/backend/models/orderModel.js b/backend/models/orderModel.js
index 1341268..91a94c2 100644
--- a/backend/models/orderModel.js
+++ b/backend/models/orderModel.js
@@ -1,14 +1,70 @@
import mongoose from "mongoose"
+const coordinateSchema = new mongoose.Schema(
+ {
+ lat: { type: Number },
+ lng: { type: Number },
+ },
+ { _id: false }
+)
+
+const orderItemSchema = new mongoose.Schema(
+ {
+ foodId: { type: mongoose.Schema.Types.ObjectId, ref: "food" },
+ name: { type: String, required: true },
+ nameEn: { type: String },
+ price: { type: Number, required: true },
+ quantity: { type: Number, required: true },
+ restaurantId: { type: mongoose.Schema.Types.ObjectId, ref: "restaurant" },
+ },
+ { _id: false }
+)
+
+const statusTimelineSchema = new mongoose.Schema(
+ {
+ status: { type: String },
+ at: { type: Date, default: Date.now },
+ restaurantId: { type: mongoose.Schema.Types.ObjectId, ref: "restaurant" },
+ },
+ { _id: false }
+)
+
+const subOrderSchema = new mongoose.Schema(
+ {
+ restaurantId: { type: mongoose.Schema.Types.ObjectId, ref: "restaurant" },
+ items: [orderItemSchema],
+ status: { type: String, default: "Food Processing" },
+ deliveryType: { type: String, enum: ["drone", "driver"] },
+ deliveryFee: { type: Number, default: 0 },
+ etaMinutes: { type: Number },
+ },
+ { _id: false }
+)
+
const orderSchema = new mongoose.Schema({
- userId:{type:String,required:true},
- items:{type:Array,required:true},
- amount:{type:Number,required:true},
+ userId:{type: mongoose.Schema.Types.ObjectId,ref:"user",required:true},
+ items:{type:[orderItemSchema],required:true},
+ amount:{type:Number,required:true}, // giữ tương thích tổng tiền cũ
address:{type:Object,required:true},
status:{type:String,default:"Food Processing"},
date:{type:Date,default:Date.now()},
- payment:{type:Boolean,default:false}
+ payment:{type:Boolean,default:false},
+ paymentMethod:{type:String, enum:["card","cod"], default:"cod"},
+ deliveryType:{type:String,enum:["drone","driver"], default:"driver"},
+ deliveryFee:{type:Number,default:0},
+ etaMinutes:{type:Number},
+ promoCode:{type:String},
+ promoDiscount:{type:Number,default:0},
+ subTotal:{type:Number},
+ total:{type:Number},
+ paymentIntentId:{type:String},
+ deliveryPhase:{type:String, enum:["at_restaurant","delivering","delivered"], default:"at_restaurant"},
+ droneId:{type:String},
+ driverId:{type:String},
+ route:{type:[coordinateSchema], default: []},
+ statusTimeline:{type:[statusTimelineSchema], default: []},
+ subOrders:{type:[subOrderSchema], default: []},
})
const orderModel = mongoose.models.order || mongoose.model("order",orderSchema)
-export default orderModel;
\ No newline at end of file
+export default orderModel;
diff --git a/backend/models/promotionModel.js b/backend/models/promotionModel.js
new file mode 100644
index 0000000..42caf20
--- /dev/null
+++ b/backend/models/promotionModel.js
@@ -0,0 +1,21 @@
+import mongoose from "mongoose";
+
+const promotionSchema = new mongoose.Schema(
+ {
+ code: { type: String, required: true, unique: true, uppercase: true },
+ type: { type: String, enum: ["percent", "amount"], required: true },
+ value: { type: Number, required: true }, // percent (0-100) hoặc số tiền giảm
+ maxDiscount: { type: Number },
+ minOrder: { type: Number, default: 0 },
+ restaurantId: { type: mongoose.Schema.Types.ObjectId, ref: "restaurant" },
+ cityId: { type: mongoose.Schema.Types.ObjectId, ref: "city" },
+ startAt: { type: Date },
+ endAt: { type: Date },
+ active: { type: Boolean, default: true },
+ },
+ { timestamps: true }
+);
+
+const promotionModel =
+ mongoose.models.promotion || mongoose.model("promotion", promotionSchema);
+export default promotionModel;
diff --git a/backend/models/restaurantModel.js b/backend/models/restaurantModel.js
new file mode 100644
index 0000000..7a41b50
--- /dev/null
+++ b/backend/models/restaurantModel.js
@@ -0,0 +1,29 @@
+import mongoose from "mongoose";
+
+const restaurantSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true },
+ nameEn: { type: String },
+ slug: { type: String, required: true, unique: true },
+ cityId: { type: mongoose.Schema.Types.ObjectId, ref: "city", required: true },
+ address: { type: String },
+ addressEn: { type: String },
+ coords: {
+ lat: { type: Number },
+ lng: { type: Number },
+ },
+ image: { type: String },
+ active: { type: Boolean, default: true },
+ deliveryModes: {
+ type: [String],
+ enum: ["drone", "driver"],
+ default: ["driver"],
+ },
+ minOrder: { type: Number, default: 0 },
+ },
+ { timestamps: true }
+);
+
+const restaurantModel =
+ mongoose.models.restaurant || mongoose.model("restaurant", restaurantSchema);
+export default restaurantModel;
diff --git a/backend/models/reviewModel.js b/backend/models/reviewModel.js
new file mode 100644
index 0000000..7bc3c24
--- /dev/null
+++ b/backend/models/reviewModel.js
@@ -0,0 +1,23 @@
+import mongoose from "mongoose";
+
+const reviewSchema = new mongoose.Schema(
+ {
+ orderId: { type: mongoose.Schema.Types.ObjectId, ref: "order", required: true },
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: "user", required: true },
+ restaurantId: { type: mongoose.Schema.Types.ObjectId, ref: "restaurant", required: true },
+ // rating tổng thể (giữ tương thích cũ)
+ rating: { type: Number, min: 1, max: 5 },
+ // rating chi tiết
+ ratingFood: { type: Number, min: 1, max: 5 },
+ ratingDriver: { type: Number, min: 1, max: 5 },
+ ratingAvg: { type: Number, min: 1, max: 5 },
+ comment: { type: String },
+ images: [{ type: String }],
+ status: { type: String, enum: ["pending", "approved", "rejected"], default: "pending" },
+ },
+ { timestamps: true }
+);
+
+const reviewModel =
+ mongoose.models.review || mongoose.model("review", reviewSchema);
+export default reviewModel;
diff --git a/backend/models/supportTicketModel.js b/backend/models/supportTicketModel.js
new file mode 100644
index 0000000..c561a7f
--- /dev/null
+++ b/backend/models/supportTicketModel.js
@@ -0,0 +1,28 @@
+import mongoose from "mongoose";
+
+const supportTicketSchema = new mongoose.Schema(
+ {
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: "user", required: true },
+ orderId: { type: mongoose.Schema.Types.ObjectId, ref: "order" },
+ subject: { type: String, required: true },
+ message: { type: String, required: true },
+ status: {
+ type: String,
+ enum: ["open", "in_progress", "resolved", "closed"],
+ default: "open",
+ },
+ replies: [
+ {
+ by: { type: String, enum: ["user", "admin"], required: true },
+ message: { type: String, required: true },
+ at: { type: Date, default: Date.now },
+ },
+ ],
+ },
+ { timestamps: true }
+);
+
+const supportTicketModel =
+ mongoose.models.supportTicket ||
+ mongoose.model("supportTicket", supportTicketSchema);
+export default supportTicketModel;
diff --git a/backend/models/userModel.js b/backend/models/userModel.js
index 4f01f58..a885c16 100644
--- a/backend/models/userModel.js
+++ b/backend/models/userModel.js
@@ -1,11 +1,17 @@
import mongoose from "mongoose"
-const userSchema = new mongoose.Schema({
- name:{type:String,required:true},
- email:{type:String,required:true,unique:true},
- password:{type:String,required:true},
- cartData:{type:Object,default:{}}
-},{minimize:false})
+const userSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true },
+ email: { type: String, required: true, unique: true },
+ password: { type: String, required: true },
+ cartData: { type: Object, default: {} },
+ role: { type: String, enum: ["user", "admin", "restaurantOwner"], default: "user" },
+ restaurantIds: [{ type: mongoose.Schema.Types.ObjectId, ref: "restaurant" }],
+ active: { type: Boolean, default: true },
+ },
+ { minimize: false, timestamps: true }
+)
const userModel = mongoose.models.user || mongoose.model("user", userSchema)
-export default userModel;
\ No newline at end of file
+export default userModel;
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 95b8592..b1b78b1 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -11,6 +11,7 @@
"dependencies": {
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
+ "cloudinary": "^2.8.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
@@ -42,9 +43,10 @@
}
},
"node_modules/@mongodb-js/saslprep": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz",
- "integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz",
+ "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==",
+ "license": "MIT",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
@@ -60,12 +62,14 @@
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
- "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+ "license": "MIT"
},
"node_modules/@types/whatwg-url": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
+ "license": "MIT",
"dependencies": {
"@types/webidl-conversions": "*"
}
@@ -233,9 +237,10 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -253,9 +258,10 @@
}
},
"node_modules/bson": {
- "version": "6.7.0",
- "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz",
- "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==",
+ "version": "6.10.4",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
+ "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
+ "license": "Apache-2.0",
"engines": {
"node": ">=16.20.1"
}
@@ -338,6 +344,19 @@
"node": ">=10"
}
},
+ "node_modules/cloudinary": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.8.0.tgz",
+ "integrity": "sha512-s7frvR0HnQXeJsQSIsbLa/I09IMb1lOnVLEDH5b5E53WTiCYgrNNOBGV/i/nLHwrcEOUkqjfSwP1+enXWNYmdw==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.21",
+ "q": "^1.5.1"
+ },
+ "engines": {
+ "node": ">=9"
+ }
+ },
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
@@ -390,9 +409,10 @@
}
},
"node_modules/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -543,16 +563,17 @@
}
},
"node_modules/express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -566,7 +587,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -581,6 +602,10 @@
},
"engines": {
"node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/fill-range": {
@@ -1003,6 +1028,12 @@
"node": ">=12.0.0"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -1071,7 +1102,8 @@
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
- "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "license": "MIT"
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
@@ -1181,24 +1213,25 @@
}
},
"node_modules/mongodb": {
- "version": "6.6.2",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.6.2.tgz",
- "integrity": "sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw==",
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
+ "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
+ "license": "Apache-2.0",
"dependencies": {
- "@mongodb-js/saslprep": "^1.1.5",
- "bson": "^6.7.0",
- "mongodb-connection-string-url": "^3.0.0"
+ "@mongodb-js/saslprep": "^1.3.0",
+ "bson": "^6.10.4",
+ "mongodb-connection-string-url": "^3.0.2"
},
"engines": {
"node": ">=16.20.1"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.188.0",
- "@mongodb-js/zstd": "^1.1.0",
+ "@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
- "snappy": "^7.2.2",
+ "snappy": "^7.3.2",
"socks": "^2.7.1"
},
"peerDependenciesMeta": {
@@ -1226,22 +1259,24 @@
}
},
"node_modules/mongodb-connection-string-url": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
- "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
+ "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
+ "license": "Apache-2.0",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
- "whatwg-url": "^13.0.0"
+ "whatwg-url": "^14.1.0 || ^13.0.0"
}
},
"node_modules/mongoose": {
- "version": "8.4.1",
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.4.1.tgz",
- "integrity": "sha512-odQ2WEWGL3hb0Qex+QMN4eH6D34WdMEw7F1If2MGABApSDmG9cMmqv/G1H6WsXmuaH9mkuuadW/WbLE5+tHJwA==",
+ "version": "8.19.3",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.3.tgz",
+ "integrity": "sha512-fTAGaIohkk8wCggMuBuqTVD4YrM1/J8cBr1ekqzFqtz65qkLjtX2dcy3NH1e+2rk2365dyrrsPAnt4YTxBhEiQ==",
+ "license": "MIT",
"dependencies": {
- "bson": "^6.7.0",
+ "bson": "^6.10.4",
"kareem": "2.6.3",
- "mongodb": "6.6.2",
+ "mongodb": "~6.20.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
@@ -1306,9 +1341,11 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/multer": {
- "version": "1.4.5-lts.1",
- "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
- "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
+ "version": "1.4.5-lts.2",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
+ "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
+ "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
+ "license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
@@ -1507,9 +1544,10 @@
}
},
"node_modules/path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -1548,10 +1586,22 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
"engines": {
"node": ">=6"
}
},
+ "node_modules/q": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+ "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
+ "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.0",
+ "teleport": ">=0.2.0"
+ }
+ },
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -1786,6 +1836,7 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+ "license": "MIT",
"dependencies": {
"memory-pager": "^1.0.2"
}
@@ -1921,14 +1972,15 @@
}
},
"node_modules/tr46": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
- "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "license": "MIT",
"dependencies": {
- "punycode": "^2.3.0"
+ "punycode": "^2.3.1"
},
"engines": {
- "node": ">=14"
+ "node": ">=18"
}
},
"node_modules/type-is": {
@@ -1980,9 +2032,10 @@
}
},
"node_modules/validator": {
- "version": "13.12.0",
- "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
- "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
+ "version": "13.15.22",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.22.tgz",
+ "integrity": "sha512-uT/YQjiyLJP7HSrv/dPZqK9L28xf8hsNca01HSz1dfmI0DgMfjopp1rO/z13NeGF1tVystF0Ejx3y4rUKPw+bQ==",
+ "license": "MIT",
"engines": {
"node": ">= 0.10"
}
@@ -1999,20 +2052,22 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
- "version": "13.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz",
- "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==",
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "license": "MIT",
"dependencies": {
- "tr46": "^4.1.1",
+ "tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
- "node": ">=16"
+ "node": ">=18"
}
},
"node_modules/wide-align": {
diff --git a/backend/package.json b/backend/package.json
index 36c10f7..8359b81 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -12,6 +12,7 @@
"dependencies": {
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
+ "cloudinary": "^2.8.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
diff --git a/backend/routes/cityRoute.js b/backend/routes/cityRoute.js
new file mode 100644
index 0000000..940eb33
--- /dev/null
+++ b/backend/routes/cityRoute.js
@@ -0,0 +1,9 @@
+import express from "express";
+import { addCity, listCities } from "../controllers/cityController.js";
+
+const cityRouter = express.Router();
+
+cityRouter.post("/add", addCity);
+cityRouter.get("/list", listCities);
+
+export default cityRouter;
diff --git a/backend/routes/foodRoute.js b/backend/routes/foodRoute.js
index 6bae49c..0aa7070 100644
--- a/backend/routes/foodRoute.js
+++ b/backend/routes/foodRoute.js
@@ -1,22 +1,34 @@
-import express from "express"
-import { addFood,listFood,removeFood } from "../controllers/foodController.js"
-import multer from "multer"
+import express from "express";
+import { addFood, listFood, removeFood } from "../controllers/foodController.js";
+import multer from "multer";
+import fs from "fs";
+import path from "path";
+import authMiddleware, { optionalAuth } from "../middleware/auth.js";
+import { requireOwnerOrAdmin } from "../middleware/access.js";
const foodRouter = express.Router();
-// Image Storage Engine
+// Tạo thư mục uploads nếu chưa có (tránh lỗi khi server restart)
+const uploadDir = path.join(process.cwd(), "uploads");
+if (!fs.existsSync(uploadDir)) {
+ fs.mkdirSync(uploadDir, { recursive: true });
+}
+// Multer Storage để lưu tạm file (Cloudinary upload nếu bật)
const storage = multer.diskStorage({
- destination:"uploads",
- filename:(req,file,cb)=>{
- return cb(null,`${Date.now()}${file.originalname}`)
- }
-})
+ destination: (req, file, cb) => cb(null, uploadDir),
+ filename: (req, file, cb) => cb(null, `${Date.now()}_${file.originalname}`),
+});
-const upload = multer({storage:storage})
+const upload = multer({ storage });
-foodRouter.post("/add",upload.single("image"),addFood)
-foodRouter.get("/list",listFood)
-foodRouter.post("/remove",removeFood);
+// POST /api/food/add (nhận file name="image")
+foodRouter.post("/add", authMiddleware, requireOwnerOrAdmin, upload.single("image"), addFood);
-export default foodRouter;
\ No newline at end of file
+// GET /api/food/list (danh sách món) — optional auth để filter cho owner
+foodRouter.get("/list", optionalAuth, listFood);
+
+// POST /api/food/remove (xóa món)
+foodRouter.post("/remove", authMiddleware, requireOwnerOrAdmin, removeFood);
+
+export default foodRouter;
diff --git a/backend/routes/orderRoute.js b/backend/routes/orderRoute.js
index dafe114..eb3f85f 100644
--- a/backend/routes/orderRoute.js
+++ b/backend/routes/orderRoute.js
@@ -1,13 +1,24 @@
-import express from "express"
-import authMiddleware from "../middleware/auth.js"
-import { placeOrder, verifyOrder, userOrders, listOrders, updateStatus } from "../controllers/orderController.js"
+import express from "express";
+import authMiddleware from "../middleware/auth.js";
+import { requireOwnerOrAdmin } from "../middleware/access.js";
+import {
+ placeOrder,
+ placeOrderCOD,
+ verifyOrder,
+ userOrders,
+ listOrders,
+ updateStatus,
+ updateDeliveryPhase,
+} from "../controllers/orderController.js";
const orderRouter = express.Router();
-orderRouter.post("/place",authMiddleware,placeOrder);
-orderRouter.post("/verify",verifyOrder)
-orderRouter.post("/userorders",authMiddleware,userOrders)
-orderRouter.get("/list",listOrders)
-orderRouter.post("/status",updateStatus)
+orderRouter.post("/place", authMiddleware, placeOrder);
+orderRouter.post("/place-cod", authMiddleware, placeOrderCOD); // << thA�m
+orderRouter.post("/verify", verifyOrder);
+orderRouter.post("/userorders", authMiddleware, userOrders);
+orderRouter.get("/list", authMiddleware, requireOwnerOrAdmin, listOrders);
+orderRouter.post("/status", authMiddleware, requireOwnerOrAdmin, updateStatus);
+orderRouter.patch("/delivery/:orderId", authMiddleware, requireOwnerOrAdmin, updateDeliveryPhase);
-export default orderRouter;
\ No newline at end of file
+export default orderRouter;
diff --git a/backend/routes/promotionRoute.js b/backend/routes/promotionRoute.js
new file mode 100644
index 0000000..cdaa9b9
--- /dev/null
+++ b/backend/routes/promotionRoute.js
@@ -0,0 +1,16 @@
+import express from "express";
+import {
+ applyPromotion,
+ createPromotion,
+ listPromotions,
+} from "../controllers/promotionController.js";
+import authMiddleware, { optionalAuth } from "../middleware/auth.js";
+import { requireOwnerOrAdmin } from "../middleware/access.js";
+
+const promotionRouter = express.Router();
+
+promotionRouter.post("/apply", applyPromotion);
+promotionRouter.post("/add", authMiddleware, requireOwnerOrAdmin, createPromotion);
+promotionRouter.get("/list", optionalAuth, listPromotions);
+
+export default promotionRouter;
diff --git a/backend/routes/restaurantRoute.js b/backend/routes/restaurantRoute.js
new file mode 100644
index 0000000..a7d6161
--- /dev/null
+++ b/backend/routes/restaurantRoute.js
@@ -0,0 +1,16 @@
+import express from "express";
+import {
+ addRestaurant,
+ listRestaurants,
+ toggleRestaurant,
+} from "../controllers/restaurantController.js";
+import authMiddleware, { optionalAuth } from "../middleware/auth.js";
+import { requireOwnerOrAdmin } from "../middleware/access.js";
+
+const restaurantRouter = express.Router();
+
+restaurantRouter.post("/add", authMiddleware, requireOwnerOrAdmin, addRestaurant);
+restaurantRouter.get("/list", optionalAuth, listRestaurants);
+restaurantRouter.post("/toggle", authMiddleware, requireOwnerOrAdmin, toggleRestaurant);
+
+export default restaurantRouter;
diff --git a/backend/routes/reviewRoute.js b/backend/routes/reviewRoute.js
new file mode 100644
index 0000000..0780f24
--- /dev/null
+++ b/backend/routes/reviewRoute.js
@@ -0,0 +1,14 @@
+import express from "express";
+import {
+ addReview,
+ listReviews,
+ approveReview,
+} from "../controllers/reviewController.js";
+
+const reviewRouter = express.Router();
+
+reviewRouter.post("/add", addReview);
+reviewRouter.get("/list", listReviews);
+reviewRouter.post("/approve", approveReview);
+
+export default reviewRouter;
diff --git a/backend/routes/stripeRoute.js b/backend/routes/stripeRoute.js
new file mode 100644
index 0000000..f0f1010
--- /dev/null
+++ b/backend/routes/stripeRoute.js
@@ -0,0 +1,57 @@
+import express from "express";
+import Stripe from "stripe";
+import orderModel from "../models/orderModel.js";
+
+const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
+const webhookRouter = express.Router();
+
+// Raw body parser is attached at server level; this handler assumes req.body is raw Buffer.
+webhookRouter.post("/webhook", async (req, res) => {
+ const sig = req.headers["stripe-signature"];
+ if (!process.env.STRIPE_WEBHOOK_SECRET) {
+ return res.status(500).json({ success: false, message: "Missing STRIPE_WEBHOOK_SECRET" });
+ }
+
+ let event;
+ try {
+ event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
+ } catch (err) {
+ console.error("Stripe webhook signature verification failed:", err.message);
+ return res.status(400).send(`Webhook Error: ${err.message}`);
+ }
+
+ try {
+ if (event.type === "checkout.session.completed") {
+ const session = event.data.object;
+ const orderId =
+ session?.metadata?.orderId ||
+ session?.client_reference_id ||
+ session?.success_url?.split("orderId=")[1];
+
+ if (orderId) {
+ await orderModel.findByIdAndUpdate(orderId, {
+ payment: true,
+ $push: { statusTimeline: { status: "Paid", at: new Date() } },
+ });
+ }
+ }
+
+ if (event.type === "payment_intent.succeeded") {
+ const pi = event.data.object;
+ const orderId = pi?.metadata?.orderId;
+ if (orderId) {
+ await orderModel.findByIdAndUpdate(orderId, {
+ payment: true,
+ $push: { statusTimeline: { status: "Paid", at: new Date() } },
+ });
+ }
+ }
+ } catch (err) {
+ console.error("Stripe webhook handler error:", err);
+ return res.status(500).json({ success: false, message: "Webhook processing error" });
+ }
+
+ return res.json({ received: true });
+});
+
+export default webhookRouter;
diff --git a/backend/routes/supportRoute.js b/backend/routes/supportRoute.js
new file mode 100644
index 0000000..c94f692
--- /dev/null
+++ b/backend/routes/supportRoute.js
@@ -0,0 +1,16 @@
+import express from "express";
+import {
+ createTicket,
+ listTickets,
+ replyTicket,
+ updateTicketStatus,
+} from "../controllers/supportController.js";
+
+const supportRouter = express.Router();
+
+supportRouter.post("/create", createTicket);
+supportRouter.get("/list", listTickets);
+supportRouter.post("/reply", replyTicket);
+supportRouter.post("/status", updateTicketStatus);
+
+export default supportRouter;
diff --git a/backend/routes/userRoute.js b/backend/routes/userRoute.js
index bed20c8..117d36a 100644
--- a/backend/routes/userRoute.js
+++ b/backend/routes/userRoute.js
@@ -1,9 +1,13 @@
import express from "express"
-import { loginUser,registerUser } from "../controllers/userController.js"
+import { loginUser,registerUser, listUsers, updateUser, removeUser } from "../controllers/userController.js"
const userRouter = express.Router()
userRouter.post("/register",registerUser)
userRouter.post("/login",loginUser)
+// Admin-ish endpoints (currently open like other admin routes)
+userRouter.get("/list", listUsers)
+userRouter.post("/update", updateUser)
+userRouter.post("/remove", removeUser)
-export default userRouter;
\ No newline at end of file
+export default userRouter;
diff --git a/backend/scripts/seedUtf8.js b/backend/scripts/seedUtf8.js
new file mode 100644
index 0000000..5e76b07
--- /dev/null
+++ b/backend/scripts/seedUtf8.js
@@ -0,0 +1,463 @@
+import mongoose from "mongoose";
+import dotenv from "dotenv";
+import bcrypt from "bcrypt";
+import cityModel from "../models/cityModel.js";
+import restaurantModel from "../models/restaurantModel.js";
+import foodModel from "../models/foodModel.js";
+import promotionModel from "../models/promotionModel.js";
+import userModel from "../models/userModel.js";
+
+dotenv.config();
+
+const uri =
+ process.env.MONGO_URI || "mongodb://127.0.0.1:27017/software_hangry";
+
+const run = async () => {
+ await mongoose.connect(uri);
+
+ // Cities
+ const hcm = await cityModel.findOneAndUpdate(
+ { code: "HCM" },
+ {
+ name: "TP. Hồ Chí Minh",
+ nameEn: "Ho Chi Minh City",
+ code: "HCM",
+ active: true,
+ },
+ { upsert: true, new: true }
+ );
+ const hanoi = await cityModel.findOneAndUpdate(
+ { code: "HN" },
+ { name: "Hà Nội", nameEn: "Hanoi", code: "HN", active: true },
+ { upsert: true, new: true }
+ );
+
+ // Restaurants
+ const rest1 = await restaurantModel.findOneAndUpdate(
+ { slug: "pho-ngon-hcm" },
+ {
+ name: "Phở Ngon HCM",
+ slug: "pho-ngon-hcm",
+ cityId: hcm._id,
+ address: "1 Lê Lợi, Q1",
+ deliveryModes: ["driver", "drone"],
+ minOrder: 50000,
+ active: true,
+ },
+ { upsert: true, new: true }
+ );
+
+ const rest2 = await restaurantModel.findOneAndUpdate(
+ { slug: "com-tam-sg" },
+ {
+ name: "Cơm Tấm Sài Gòn",
+ slug: "com-tam-sg",
+ cityId: hcm._id,
+ address: "45 Nguyễn Trãi, Q5",
+ deliveryModes: ["driver"],
+ minOrder: 40000,
+ active: true,
+ },
+ { upsert: true, new: true }
+ );
+
+ const rest3 = await restaurantModel.findOneAndUpdate(
+ { slug: "banh-mi-hn" },
+ {
+ name: "Bánh Mì HN",
+ slug: "banh-mi-hn",
+ cityId: hanoi._id,
+ address: "10 Tràng Tiền, Hoàn Kiếm",
+ deliveryModes: ["driver"],
+ minOrder: 30000,
+ active: true,
+ },
+ { upsert: true, new: true }
+ );
+
+ const rest4 = await restaurantModel.findOneAndUpdate(
+ { slug: "am-thuc-hn" },
+ {
+ name: "Ẩm Thực Hà Nội",
+ slug: "am-thuc-hn",
+ cityId: hanoi._id,
+ address: "25 Phan Chu Trinh, Hoàn Kiếm",
+ deliveryModes: ["driver", "drone"],
+ minOrder: 50000,
+ active: true,
+ },
+ { upsert: true, new: true }
+ );
+
+ // Foods (VI/EN)
+ const foods = [
+ // HCM - Phở Ngon
+ {
+ name: "Salad Tôm",
+ nameEn: "Shrimp Salad",
+ description: "Shrimp salad, refreshing",
+ descriptionVi: "Salad tôm thanh mát",
+ price: 55000,
+ category: "Salad",
+ restaurantId: rest1._id,
+ cityId: hcm._id,
+ image: "1727126978673food_1.png",
+ },
+ {
+ name: "Gỏi Cuốn",
+ nameEn: "Fresh Spring Rolls",
+ description: "Fresh spring rolls",
+ descriptionVi: "Gỏi cuốn tôm thịt",
+ price: 35000,
+ category: "Rolls",
+ restaurantId: rest1._id,
+ cityId: hcm._id,
+ image: "1727127544604food_3.png",
+ },
+ {
+ name: "Bánh Flan",
+ nameEn: "Cream Caramel",
+ description: "Cream caramel",
+ descriptionVi: "Bánh flan mềm mịn",
+ price: 30000,
+ category: "Deserts",
+ restaurantId: rest1._id,
+ cityId: hcm._id,
+ image: "1727127773554food_11.png",
+ },
+ {
+ name: "Bánh Mì Chảo",
+ nameEn: "Vietnamese Pan Bread",
+ description: "Vietnamese pan bread",
+ descriptionVi: "Bánh mì chảo kiểu Việt",
+ price: 45000,
+ category: "Sandwich",
+ restaurantId: rest1._id,
+ cityId: hcm._id,
+ image: "1727127695985food_8.png",
+ },
+ {
+ name: "Mì Ý Bò",
+ nameEn: "Beef Pasta",
+ description: "Beef pasta",
+ descriptionVi: "Mì Ý bò bằm",
+ price: 75000,
+ category: "Pasta",
+ restaurantId: rest1._id,
+ cityId: hcm._id,
+ image: "1727128540374food_26.png",
+ },
+ {
+ name: "Phở Bò",
+ nameEn: "Pho Beef",
+ description: "Pho with beef",
+ descriptionVi: "Phở bò truyền thống",
+ price: 60000,
+ category: "Noodles",
+ restaurantId: rest1._id,
+ cityId: hcm._id,
+ image: "1727128122765food_19.png",
+ },
+
+ // HCM - Cơm Tấm SG
+ {
+ name: "Cơm Tấm Sườn",
+ nameEn: "Broken Rice with Pork",
+ description: "Broken rice with grilled pork",
+ descriptionVi: "Cơm tấm sườn bì chả",
+ price: 70000,
+ category: "Pure Veg",
+ restaurantId: rest2._id,
+ cityId: hcm._id,
+ image: "1727128584690food_25.png",
+ },
+ {
+ name: "Cơm Gà Xối Mỡ",
+ nameEn: "Fried Chicken Rice",
+ description: "Fried chicken rice",
+ descriptionVi: "Cơm gà xối mỡ",
+ price: 65000,
+ category: "Salad",
+ restaurantId: rest2._id,
+ cityId: hcm._id,
+ image: "1727127730519food_9.png",
+ },
+ {
+ name: "Bánh Mì Thịt",
+ nameEn: "Vietnamese Baguette",
+ description: "Baguette with cold cuts",
+ descriptionVi: "Bánh mì thịt nguội",
+ price: 35000,
+ category: "Sandwich",
+ restaurantId: rest2._id,
+ cityId: hcm._id,
+ image: "1727127750882food_10.png",
+ },
+ {
+ name: "Bánh Bông Lan",
+ nameEn: "Sponge Cake",
+ description: "Sponge cake",
+ descriptionVi: "Bánh bông lan mềm",
+ price: 40000,
+ category: "Cake",
+ restaurantId: rest2._id,
+ cityId: hcm._id,
+ image: "1727128017975food_15.png",
+ },
+ {
+ name: "Miến Trộn",
+ nameEn: "Glass Noodle Salad",
+ description: "Glass noodle salad",
+ descriptionVi: "Miến trộn kiểu Việt",
+ price: 55000,
+ category: "Noodles",
+ restaurantId: rest2._id,
+ cityId: hcm._id,
+ image: "1727128312994food_21.png",
+ },
+ {
+ name: "Pasta Hải Sản",
+ nameEn: "Seafood Pasta",
+ description: "Seafood pasta",
+ descriptionVi: "Mì Ý hải sản",
+ price: 80000,
+ category: "Pasta",
+ restaurantId: rest2._id,
+ cityId: hcm._id,
+ image: "1727128445234food_24.png",
+ },
+
+ // Hà Nội - Bánh Mì HN
+ {
+ name: "Bánh Mì Truyền Thống",
+ nameEn: "Classic Baguette",
+ description: "Classic baguette",
+ descriptionVi: "Bánh mì thịt truyền thống",
+ price: 30000,
+ category: "Sandwich",
+ restaurantId: rest3._id,
+ cityId: hanoi._id,
+ image: "1727127794307food_12.png",
+ },
+ {
+ name: "Bánh Mì Gà Nướng",
+ nameEn: "Grilled Chicken Baguette",
+ description: "Grilled chicken baguette",
+ descriptionVi: "Bánh mì gà nướng",
+ price: 32000,
+ category: "Sandwich",
+ restaurantId: rest3._id,
+ cityId: hanoi._id,
+ image: "1727127837323food_13.png",
+ },
+ {
+ name: "Nem Rán",
+ nameEn: "Fried Spring Rolls",
+ description: "Fried spring rolls",
+ descriptionVi: "Nem rán Hà Nội",
+ price: 40000,
+ category: "Rolls",
+ restaurantId: rest3._id,
+ cityId: hanoi._id,
+ image: "1727127988392food_14.png",
+ },
+ {
+ name: "Salad Gà",
+ nameEn: "Chicken Salad",
+ description: "Chicken salad",
+ descriptionVi: "Salad gà tươi",
+ price: 45000,
+ category: "Salad",
+ restaurantId: rest3._id,
+ cityId: hanoi._id,
+ image: "1727127590415food_4.png",
+ },
+ {
+ name: "Bánh Ngọt Mix",
+ nameEn: "Mixed Desserts",
+ description: "Mixed desserts",
+ descriptionVi: "Set bánh ngọt mix",
+ price: 38000,
+ category: "Cake",
+ restaurantId: rest3._id,
+ cityId: hanoi._id,
+ image: "1727128078923food_17.png",
+ },
+ {
+ name: "Bún Chả",
+ nameEn: "Bun Cha",
+ description: "Grilled pork with vermicelli",
+ descriptionVi: "Bún chả Hà Nội",
+ price: 65000,
+ category: "Noodles",
+ restaurantId: rest3._id,
+ cityId: hanoi._id,
+ image: "1727128045565food_16.png",
+ },
+
+ // Hà Nội - Ẩm Thực Hà Nội
+ {
+ name: "Bún Chả Đặc Sản",
+ nameEn: "Hanoi Bun Cha",
+ description: "Signature bun cha",
+ descriptionVi: "Bún chả đặc sản",
+ price: 65000,
+ category: "Noodles",
+ restaurantId: rest4._id,
+ cityId: hanoi._id,
+ image: "1727128102874food_18.png",
+ },
+ {
+ name: "Chả Cá Lã Vọng",
+ nameEn: "Cha Ca La Vong",
+ description: "Hanoi turmeric fish",
+ descriptionVi: "Chả cá Lã Vọng",
+ price: 70000,
+ category: "Pure Veg",
+ restaurantId: rest4._id,
+ cityId: hanoi._id,
+ image: "1727128392133food_23.png",
+ },
+ {
+ name: "Phở Cuốn",
+ nameEn: "Pho Rolls",
+ description: "Pho rolls",
+ descriptionVi: "Phở cuốn Hà Nội",
+ price: 50000,
+ category: "Rolls",
+ restaurantId: rest4._id,
+ cityId: hanoi._id,
+ image: "1727128219303food_20.png",
+ },
+ {
+ name: "Miến Lươn",
+ nameEn: "Eel Glass Noodles",
+ description: "Glass noodles with eel",
+ descriptionVi: "Miến lươn Nghệ",
+ price: 60000,
+ category: "Noodles",
+ restaurantId: rest4._id,
+ cityId: hanoi._id,
+ image: "1727128347200food_22.png",
+ },
+ {
+ name: "Bánh Trôi Tàu",
+ nameEn: "Sweet Rice Balls",
+ description: "Sweet glutinous rice balls",
+ descriptionVi: "Bánh trôi tàu ngọt",
+ price: 40000,
+ category: "Deserts",
+ restaurantId: rest4._id,
+ cityId: hanoi._id,
+ image: "1727127642374food_6.png",
+ },
+ {
+ name: "Pasta Chay",
+ nameEn: "Veggie Pasta",
+ description: "Vegetarian pasta",
+ descriptionVi: "Mì Ý chay rau củ",
+ price: 65000,
+ category: "Pasta",
+ restaurantId: rest4._id,
+ cityId: hanoi._id,
+ image: "1727128584690food_25.png",
+ },
+ ];
+
+ for (const f of foods) {
+ await foodModel.findOneAndUpdate(
+ { name: f.name, restaurantId: f.restaurantId },
+ {
+ ...f,
+ image: f.image || "https://via.placeholder.com/400x300?text=Food",
+ },
+ { upsert: true, new: true }
+ );
+ }
+
+ // Owners per restaurant
+ const ownerSeeds = [
+ {
+ email: "owner.pho@example.com",
+ password: "Pho@123",
+ name: "Chủ Phở Ngon",
+ restaurantIds: [rest1._id],
+ },
+ {
+ email: "owner.comtam@example.com",
+ password: "ComTam@123",
+ name: "Chủ Cơm Tấm",
+ restaurantIds: [rest2._id],
+ },
+ {
+ email: "owner.banhmi@example.com",
+ password: "BanhMi@123",
+ name: "Chủ Bánh Mì",
+ restaurantIds: [rest3._id],
+ },
+ {
+ email: "owner.amthuc@example.com",
+ password: "AmThuc@123",
+ name: "Chủ Ẩm Thực",
+ restaurantIds: [rest4._id],
+ },
+ ];
+
+ for (const owner of ownerSeeds) {
+ const hashed = await bcrypt.hash(owner.password, 10);
+ await userModel.findOneAndUpdate(
+ { email: owner.email },
+ {
+ name: owner.name,
+ email: owner.email,
+ password: hashed,
+ role: "restaurantOwner",
+ restaurantIds: owner.restaurantIds,
+ active: true,
+ },
+ { upsert: true, new: true }
+ );
+ }
+
+ // Admin account
+ const adminEmail = "admin@example.com";
+ const adminPassword = "Admin@123";
+ const adminHash = await bcrypt.hash(adminPassword, 10);
+ await userModel.findOneAndUpdate(
+ { email: adminEmail },
+ {
+ name: "Super Admin",
+ email: adminEmail,
+ password: adminHash,
+ role: "admin",
+ active: true,
+ },
+ { upsert: true, new: true }
+ );
+
+ await promotionModel.findOneAndUpdate(
+ { code: "WELCOME10" },
+ {
+ code: "WELCOME10",
+ type: "percent",
+ value: 10,
+ maxDiscount: 30000,
+ minOrder: 80000,
+ active: true,
+ cityId: hcm._id,
+ restaurantId: rest1._id,
+ },
+ { upsert: true, new: true }
+ );
+
+ console.log("Seed done (VI/EN, 2 cities x 2 restaurants x 6 món mỗi quán).");
+ console.log("Owners:");
+ ownerSeeds.forEach((o) => console.log(`- ${o.email} / ${o.password}`));
+ console.log(`Admin: ${adminEmail} / ${adminPassword}`);
+ process.exit(0);
+};
+
+run().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/backend/server.js b/backend/server.js
index f756ec8..8f3df68 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -1,5 +1,9 @@
+// ...existing code...
import dotenv from 'dotenv';
dotenv.config();
+// import cloudinary config (optional) so credential check happens early
+import "./config/cloudinary.js";
+
import express from "express"
import cors from "cors"
import { connectDB } from "./config/db.js"
@@ -8,35 +12,73 @@ import userRouter from "./routes/userRoute.js"
import 'dotenv/config'
import cartRouter from "./routes/cartRoute.js"
import orderRouter from "./routes/orderRoute.js"
+import cityRouter from "./routes/cityRoute.js"
+import restaurantRouter from "./routes/restaurantRoute.js"
+import promotionRouter from "./routes/promotionRoute.js"
+import reviewRouter from "./routes/reviewRoute.js"
+import supportRouter from "./routes/supportRoute.js"
+import stripeWebhookRouter from "./routes/stripeRoute.js"
// app config
const app = express()
const port = process.env.PORT || 4000
-// middleware
+// Stripe webhook: must be raw before json parser
+app.use("/api/stripe", express.raw({ type: "application/json" }), stripeWebhookRouter)
+
+// CORS: use CLIENT_URL env (comma-separated) or fallback to common dev origins
+const rawOrigins = process.env.CLIENT_URL || "http://localhost:8081,http://127.0.0.1:8081,http://localhost:3000";
+const allowedOrigins = rawOrigins.split(",").map(s => s.trim());
+
app.use(express.json())
-app.use(cors())
+app.use(cors({
+ origin: (origin, callback) => {
+ // allow requests with no origin (mobile apps, curl, Postman)
+ if (!origin) return callback(null, true);
+ if (allowedOrigins.includes(origin)) return callback(null, true);
+ return callback(new Error("Not allowed by CORS"));
+ },
+ credentials: true
+}));
// db connection
connectDB();
// api endpoints
-app.use("/api/food",foodRouter)
-app.use("/images",express.static('uploads'))
-app.use("/api/user",userRouter)
-app.use("/api/cart",cartRouter)
-app.use("/api/order",orderRouter)
+app.use("/api/food", foodRouter)
+app.use("/images", express.static("uploads", {
+ setHeaders: (res, path, stat) => {
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ },
+}));
+app.use("/api/user", userRouter)
+app.use("/api/cart", cartRouter)
+app.use("/api/order", orderRouter)
+app.use("/api/city", cityRouter)
+app.use("/api/restaurant", restaurantRouter)
+app.use("/api/promo", promotionRouter)
+app.use("/api/review", reviewRouter)
+app.use("/api/support", supportRouter)
-
-app.get("/",(req,res)=>{
- res.send("API Working")
-})
-
-app.listen(port,()=>{
- console.log(`Server Started on http://localhost:${port}`)
+app.get("/", (req, res) => {
+ res.send("API Working")
})
-// YOU CAN SAVE UR DATABASE IN THIS COMMENT IF U WANT -->
+// listen with basic error handling
+const server = app.listen(port, () => {
+ console.log(`Server Started on http://localhost:${port}`)
+ console.log(`[CORS] allowed origins: ${allowedOrigins.join(", ")}`)
+});
+server.on('error', (err) => {
+ if (err && err.code === 'EADDRINUSE') {
+ console.error(`Port ${port} is already in use. Kill the process using it or set a different PORT.`);
+ process.exit(1);
+ } else {
+ console.error('Server error:', err);
+ process.exit(1);
+ }
+});
+// ...existing code...
diff --git a/backend/uploads/eat.png b/backend/uploads/eat.png
new file mode 100644
index 0000000..18f4bad
Binary files /dev/null and b/backend/uploads/eat.png differ
diff --git a/backend/uploads/food_item.png b/backend/uploads/food_item.png
new file mode 100644
index 0000000..a2059f0
Binary files /dev/null and b/backend/uploads/food_item.png differ
diff --git a/backend/uploads/food_item_2.png b/backend/uploads/food_item_2.png
new file mode 100644
index 0000000..da0a28b
Binary files /dev/null and b/backend/uploads/food_item_2.png differ
diff --git a/documents/Danh-Phuoc-GitHub.txt b/documents/Danh-Phuoc-GitHub.txt
new file mode 100644
index 0000000..3f1d1a1
--- /dev/null
+++ b/documents/Danh-Phuoc-GitHub.txt
@@ -0,0 +1,2 @@
+Danh-Phuoc-GitHub
+https://github.com/TNTDanh/Software_Hangry
\ No newline at end of file
diff --git a/documents/NHOM-2_FOODFAST_DELIVERY-HANGRY_PRD.pdf b/documents/NHOM-2_FOODFAST_DELIVERY-HANGRY_PRD.pdf
new file mode 100644
index 0000000..20afccc
Binary files /dev/null and b/documents/NHOM-2_FOODFAST_DELIVERY-HANGRY_PRD.pdf differ
diff --git a/frontend/index.html b/frontend/index.html
index dc9777c..eab38f8 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
- D&P RESTAURANT
+ DANHPHUOCxFOODFAST
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 8667a96..3e294ac 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -22,60 +22,51 @@
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
- "vite": "^5.2.0"
- }
- },
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "dev": true,
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
+ "vite": "^7.2.2"
}
},
"node_modules/@babel/code-frame": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz",
- "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/highlight": "^7.24.6",
- "picocolors": "^1.0.0"
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/compat-data": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz",
- "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz",
- "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==",
- "dev": true,
- "dependencies": {
- "@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.24.6",
- "@babel/generator": "^7.24.6",
- "@babel/helper-compilation-targets": "^7.24.6",
- "@babel/helper-module-transforms": "^7.24.6",
- "@babel/helpers": "^7.24.6",
- "@babel/parser": "^7.24.6",
- "@babel/template": "^7.24.6",
- "@babel/traverse": "^7.24.6",
- "@babel/types": "^7.24.6",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -91,29 +82,32 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz",
- "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.24.6",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
- "jsesc": "^2.5.1"
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz",
- "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.24.6",
- "@babel/helper-validator-option": "^7.24.6",
- "browserslist": "^4.22.2",
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
@@ -121,63 +115,40 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/helper-environment-visitor": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz",
- "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-function-name": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz",
- "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==",
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
- "dependencies": {
- "@babel/template": "^7.24.6",
- "@babel/types": "^7.24.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-hoist-variables": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz",
- "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==",
- "dev": true,
- "dependencies": {
- "@babel/types": "^7.24.6"
- },
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz",
- "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.24.6"
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz",
- "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/helper-environment-visitor": "^7.24.6",
- "@babel/helper-module-imports": "^7.24.6",
- "@babel/helper-simple-access": "^7.24.6",
- "@babel/helper-split-export-declaration": "^7.24.6",
- "@babel/helper-validator-identifier": "^7.24.6"
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
},
"engines": {
"node": ">=6.9.0"
@@ -187,98 +158,68 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz",
- "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-simple-access": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz",
- "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==",
- "dev": true,
- "dependencies": {
- "@babel/types": "^7.24.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-split-export-declaration": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz",
- "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
"dev": true,
- "dependencies": {
- "@babel/types": "^7.24.6"
- },
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz",
- "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz",
- "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz",
- "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz",
- "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/template": "^7.24.6",
- "@babel/types": "^7.24.6"
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
- "node_modules/@babel/highlight": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz",
- "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==",
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.24.6",
- "chalk": "^2.4.2",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
+ "@babel/types": "^7.28.5"
},
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz",
- "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==",
- "dev": true,
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -287,12 +228,13 @@
}
},
"node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.6.tgz",
- "integrity": "sha512-FfZfHXtQ5jYPQsCRyLpOv2GeLIIJhs8aydpNh39vRDjhD411XcfWDni5i7OjP/Rs8GAtTn7sWFFELJSHqkIxYg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.6"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -302,12 +244,13 @@
}
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.6.tgz",
- "integrity": "sha512-BQTBCXmFRreU3oTUXcGKuPOfXAGb1liNY4AvvFKsOBAJ89RKcTsIrSsnMYkj59fNa66OFKnSa4AJZfy5Y4B9WA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.6"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -317,420 +260,493 @@
}
},
"node_modules/@babel/template": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz",
- "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.24.6",
- "@babel/parser": "^7.24.6",
- "@babel/types": "^7.24.6"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz",
- "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==",
- "dev": true,
- "dependencies": {
- "@babel/code-frame": "^7.24.6",
- "@babel/generator": "^7.24.6",
- "@babel/helper-environment-visitor": "^7.24.6",
- "@babel/helper-function-name": "^7.24.6",
- "@babel/helper-hoist-variables": "^7.24.6",
- "@babel/helper-split-export-declaration": "^7.24.6",
- "@babel/parser": "^7.24.6",
- "@babel/types": "^7.24.6",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz",
- "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.24.6",
- "@babel/helper-validator-identifier": "^7.24.6",
- "to-fast-properties": "^2.0.0"
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -838,17 +854,25 @@
"dev": true
},
"node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
- "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@@ -856,30 +880,24 @@
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.4.15",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
- "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
- "dev": true
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -928,209 +946,316 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
- "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz",
+ "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
- "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz",
+ "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
- "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz",
+ "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
- "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz",
+ "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz",
+ "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz",
+ "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
- "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz",
+ "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
- "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz",
+ "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
- "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz",
+ "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
- "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz",
+ "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz",
+ "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
- "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz",
+ "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==",
"cpu": [
"ppc64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
- "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz",
+ "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==",
"cpu": [
"riscv64"
],
"dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz",
+ "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
- "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz",
+ "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==",
"cpu": [
"s390x"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
- "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz",
+ "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
- "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz",
+ "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz",
+ "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
- "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz",
+ "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
- "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz",
+ "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==",
"cpu": [
"ia32"
],
"dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz",
+ "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
- "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz",
+ "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -1178,10 +1303,11 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
- "dev": true
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.12",
@@ -1215,22 +1341,24 @@
"dev": true
},
"node_modules/@vitejs/plugin-react": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.0.tgz",
- "integrity": "sha512-KcEbMsn4Dpk+LIbHMj7gDPRKaTMStxxWRkRmxsg/jVdFdJCZWt1SchZcf0M4t8lIKdwwMsEyzhrcOXRrDPtOBw==",
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/core": "^7.24.5",
- "@babel/plugin-transform-react-jsx-self": "^7.24.5",
- "@babel/plugin-transform-react-jsx-source": "^7.24.1",
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
"@types/babel__core": "^7.20.5",
- "react-refresh": "^0.14.2"
+ "react-refresh": "^0.17.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0"
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/acorn": {
@@ -1279,18 +1407,6 @@
"node": ">=8"
}
},
- "node_modules/ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dev": true,
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1442,7 +1558,8 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
@@ -1460,12 +1577,13 @@
}
},
"node_modules/axios": {
- "version": "1.7.7",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
- "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -1475,20 +1593,31 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.25",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz",
+ "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/browserslist": {
- "version": "4.23.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
- "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+ "version": "4.27.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
+ "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
"dev": true,
"funding": [
{
@@ -1504,11 +1633,13 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001587",
- "electron-to-chromium": "^1.4.668",
- "node-releases": "^2.0.14",
- "update-browserslist-db": "^1.0.13"
+ "baseline-browser-mapping": "^2.8.19",
+ "caniuse-lite": "^1.0.30001751",
+ "electron-to-chromium": "^1.5.238",
+ "node-releases": "^2.0.26",
+ "update-browserslist-db": "^1.1.4"
},
"bin": {
"browserslist": "cli.js"
@@ -1536,6 +1667,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1546,9 +1690,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001627",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001627.tgz",
- "integrity": "sha512-4zgNiB8nTyV/tHhwZrFs88ryjls/lHiqFhrxCW4qSTeuRByBVnPYpDInchOIySWknznucaf31Z4KYqjfbrecVw==",
+ "version": "1.0.30001754",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
+ "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
"dev": true,
"funding": [
{
@@ -1563,41 +1707,14 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
- },
- "node_modules/chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dev": true,
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dev": true,
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "dev": true
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -1615,13 +1732,15 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -1749,6 +1868,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
"engines": {
"node": ">=0.4.0"
}
@@ -1765,11 +1885,26 @@
"node": ">=6.0.0"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/electron-to-chromium": {
- "version": "1.4.788",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.788.tgz",
- "integrity": "sha512-ubp5+Ev/VV8KuRoWnfP2QF2Bg+O2ZFdb49DiiNbz2VmgkIqrnyYaqIOqj8A6K/3p1xV0QcU5hBQ1+BmB6ot1OA==",
- "dev": true
+ "version": "1.5.249",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.249.tgz",
+ "integrity": "sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/es-abstract": {
"version": "1.23.3",
@@ -1832,13 +1967,10 @@
}
},
"node_modules/es-define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
- "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
- "dev": true,
- "dependencies": {
- "get-intrinsic": "^1.2.4"
- },
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
}
@@ -1847,7 +1979,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -1878,10 +2009,10 @@
}
},
"node_modules/es-object-atoms": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
- "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
- "dev": true,
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
@@ -1890,14 +2021,15 @@
}
},
"node_modules/es-set-tostringtag": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
- "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
- "dev": true,
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
"dependencies": {
- "get-intrinsic": "^1.2.4",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
- "hasown": "^2.0.1"
+ "hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -1930,61 +2062,57 @@
}
},
"node_modules/esbuild": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/escalade": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
- "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
},
- "node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true,
- "engines": {
- "node": ">=0.8.0"
- }
- },
"node_modules/eslint": {
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
@@ -2316,6 +2444,24 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -2393,12 +2539,15 @@
}
},
"node_modules/form-data": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
- "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -2417,6 +2566,7 @@
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -2429,7 +2579,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -2466,21 +2615,27 @@
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/get-intrinsic": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
- "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
- "dev": true,
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
"dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0"
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -2489,6 +2644,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-symbol-description": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
@@ -2539,15 +2707,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/globalthis": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
@@ -2565,12 +2724,12 @@
}
},
"node_modules/gopd": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
- "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
- "dev": true,
- "dependencies": {
- "get-intrinsic": "^1.1.3"
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -2591,15 +2750,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
@@ -2625,10 +2775,10 @@
}
},
"node_modules/has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
- "dev": true,
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -2640,7 +2790,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@@ -2655,7 +2804,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -3108,15 +3256,16 @@
}
},
"node_modules/jsesc": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
- "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
+ "license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
- "node": ">=4"
+ "node": ">=6"
}
},
"node_modules/json-buffer": {
@@ -3142,6 +3291,7 @@
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
+ "license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
@@ -3223,14 +3373,25 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -3239,6 +3400,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
@@ -3265,9 +3427,9 @@
"dev": true
},
"node_modules/nanoid": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
@@ -3275,6 +3437,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -3289,10 +3452,11 @@
"dev": true
},
"node_modules/node-releases": {
- "version": "2.0.14",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
- "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
- "dev": true
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
@@ -3507,10 +3671,24 @@
"dev": true
},
"node_modules/picocolors": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
- "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
- "dev": true
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
},
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
@@ -3522,9 +3700,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.47",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
- "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
@@ -3540,9 +3718,10 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.1.0",
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
@@ -3642,10 +3821,11 @@
"dev": true
},
"node_modules/react-refresh": {
- "version": "0.14.2",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
- "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -3772,12 +3952,13 @@
}
},
"node_modules/rollup": {
- "version": "4.22.4",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
- "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
+ "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@types/estree": "1.0.5"
+ "@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -3787,22 +3968,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.22.4",
- "@rollup/rollup-android-arm64": "4.22.4",
- "@rollup/rollup-darwin-arm64": "4.22.4",
- "@rollup/rollup-darwin-x64": "4.22.4",
- "@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
- "@rollup/rollup-linux-arm-musleabihf": "4.22.4",
- "@rollup/rollup-linux-arm64-gnu": "4.22.4",
- "@rollup/rollup-linux-arm64-musl": "4.22.4",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
- "@rollup/rollup-linux-riscv64-gnu": "4.22.4",
- "@rollup/rollup-linux-s390x-gnu": "4.22.4",
- "@rollup/rollup-linux-x64-gnu": "4.22.4",
- "@rollup/rollup-linux-x64-musl": "4.22.4",
- "@rollup/rollup-win32-arm64-msvc": "4.22.4",
- "@rollup/rollup-win32-ia32-msvc": "4.22.4",
- "@rollup/rollup-win32-x64-msvc": "4.22.4",
+ "@rollup/rollup-android-arm-eabi": "4.53.2",
+ "@rollup/rollup-android-arm64": "4.53.2",
+ "@rollup/rollup-darwin-arm64": "4.53.2",
+ "@rollup/rollup-darwin-x64": "4.53.2",
+ "@rollup/rollup-freebsd-arm64": "4.53.2",
+ "@rollup/rollup-freebsd-x64": "4.53.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.2",
+ "@rollup/rollup-linux-arm64-musl": "4.53.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.2",
+ "@rollup/rollup-linux-x64-gnu": "4.53.2",
+ "@rollup/rollup-linux-x64-musl": "4.53.2",
+ "@rollup/rollup-openharmony-arm64": "4.53.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.2",
+ "@rollup/rollup-win32-x64-gnu": "4.53.2",
+ "@rollup/rollup-win32-x64-msvc": "4.53.2",
"fsevents": "~2.3.2"
}
},
@@ -3957,6 +4144,7 @@
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
+ "license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -4060,18 +4248,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -4090,13 +4266,21 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
- "node_modules/to-fast-properties": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
- "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
"engines": {
- "node": ">=4"
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/type-check": {
@@ -4212,9 +4396,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.0.16",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
- "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
+ "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
"dev": true,
"funding": [
{
@@ -4230,9 +4414,10 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "escalade": "^3.1.2",
- "picocolors": "^1.0.1"
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
@@ -4251,20 +4436,24 @@
}
},
"node_modules/vite": {
- "version": "5.4.7",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz",
- "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==",
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
+ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
- "node": "^18.0.0 || >=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -4273,19 +4462,25 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
- "less": "*",
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
"lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.4.0"
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
+ "jiti": {
+ "optional": true
+ },
"less": {
"optional": true
},
@@ -4306,6 +4501,12 @@
},
"terser": {
"optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
}
}
},
@@ -4422,7 +4623,8 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true
+ "dev": true,
+ "license": "ISC"
},
"node_modules/yocto-queue": {
"version": "0.1.0",
diff --git a/frontend/package.json b/frontend/package.json
index c76e7de..5f01e62 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -24,6 +24,6 @@
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
- "vite": "^5.2.0"
+ "vite": "^7.2.2"
}
}
diff --git a/frontend/public/food_item_2.png b/frontend/public/food_item_2.png
new file mode 100644
index 0000000..da0a28b
Binary files /dev/null and b/frontend/public/food_item_2.png differ
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 4076642..7cd6640 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,37 +1,40 @@
-import React from 'react'
+import React, { useState } from 'react'
import Navbar from './components/Navbar/Navbar'
import { Route, Routes } from 'react-router-dom'
import Home from './pages/Home/Home'
import Cart from './pages/Cart/Cart'
import PlaceOrder from './pages/PlaceOrder/PlaceOrder'
import Footer from './components/Footer/Footer'
-import { useState } from 'react'
import LoginPopup from './components/LoginPopup/LoginPopup'
import Verify from './pages/Verify/Verify'
import MyOrders from './pages/MyOrders/MyOrders'
+import Profile from './pages/Profile/Profile'
+import ScrollToHash from './components/ScrollToHash'
console.log('%cWhy are you looking at my logs 👀', 'color: #FF5733; font-size: 20px;');
const App = () => {
-
- const [showLogin,setShowLogin] = useState(false)
+ const [showLogin, setShowLogin] = useState(false)
return (
<>
- {showLogin?:<>>}
-
-
+ {showLogin ?
: null}
+
+
} />
} />
} />
} />
} />
+ } />
+ } />
+
>
)
}
-export default App
\ No newline at end of file
+export default App
diff --git a/frontend/src/assets/eat.png b/frontend/src/assets/eat.png
new file mode 100644
index 0000000..18f4bad
Binary files /dev/null and b/frontend/src/assets/eat.png differ
diff --git a/frontend/src/assets/food_item.png b/frontend/src/assets/food_item.png
new file mode 100644
index 0000000..a2059f0
Binary files /dev/null and b/frontend/src/assets/food_item.png differ
diff --git a/frontend/src/assets/food_item_2.png b/frontend/src/assets/food_item_2.png
new file mode 100644
index 0000000..da0a28b
Binary files /dev/null and b/frontend/src/assets/food_item_2.png differ
diff --git a/frontend/src/assets/localImages.js b/frontend/src/assets/localImages.js
new file mode 100644
index 0000000..7ac3b60
--- /dev/null
+++ b/frontend/src/assets/localImages.js
@@ -0,0 +1,56 @@
+import food_1 from "./food_1.png";
+import food_2 from "./food_2.png";
+import food_3 from "./food_3.png";
+import food_4 from "./food_4.png";
+import food_5 from "./food_5.png";
+import food_6 from "./food_6.png";
+import food_7 from "./food_7.png";
+import food_8 from "./food_8.png";
+import food_9 from "./food_9.png";
+import food_10 from "./food_10.png";
+import food_11 from "./food_11.png";
+import food_12 from "./food_12.png";
+import food_13 from "./food_13.png";
+import food_14 from "./food_14.png";
+import food_15 from "./food_15.png";
+import food_16 from "./food_16.png";
+import food_17 from "./food_17.png";
+import food_18 from "./food_18.png";
+import food_19 from "./food_19.png";
+import food_20 from "./food_20.png";
+import food_21 from "./food_21.png";
+import food_22 from "./food_22.png";
+import food_23 from "./food_23.png";
+import food_24 from "./food_24.png";
+import food_25 from "./food_25.png";
+import food_26 from "./food_26.png";
+
+// Map tên file từ backend/uploads -> local asset tương ứng
+export const LOCAL_MAP = {
+ "1727126978673food_1.png": food_1,
+ "1727127509822food_2.png": food_2,
+ "1727127544604food_3.png": food_3,
+ "1727127590415food_4.png": food_4,
+ "1727127618294food_5.png": food_5,
+ "1727127642374food_6.png": food_6,
+ "1727127661470food_7.png": food_7,
+ "1727127695985food_8.png": food_8,
+ "1727127730519food_9.png": food_9,
+ "1727127750882food_10.png": food_10,
+ "1727127773554food_11.png": food_11,
+ "1727127794307food_12.png": food_12,
+ "1727127837323food_13.png": food_13,
+ "1727127988392food_14.png": food_14,
+ "1727128017975food_15.png": food_15,
+ "1727128045565food_16.png": food_16,
+ "1727128078923food_17.png": food_17,
+ "1727128102874food_18.png": food_18,
+ "1727128122765food_19.png": food_19,
+ "1727128219303food_20.png": food_20,
+ "1727128312994food_21.png": food_21,
+ "1727128347200food_22.png": food_22,
+ "1727128392133food_23.png": food_23,
+ "1727128445234food_24.png": food_24,
+ "1727128584690food_25.png": food_25,
+ "1727128540374food_26.png": food_26,
+};
diff --git a/frontend/src/components/AppDownload/AppDownload.css b/frontend/src/components/AppDownload/AppDownload.css
index c4ecf31..096739d 100644
--- a/frontend/src/components/AppDownload/AppDownload.css
+++ b/frontend/src/components/AppDownload/AppDownload.css
@@ -1,30 +1,56 @@
-.app-download{
- margin: auto auto;
- margin-top: 100px;
- font-size: max(3vw, 20px);
- text-align: center;
- font-weight: 500;
-}
-.app-download-platforms{
- display: flex;
- justify-content: center;
- gap: max(2vw, 10px);
- margin-top: 40px;
-}
-.app-download-platforms img{
- width: max(30vw, 120px);
- max-width: 180px;
- transition: 0.5s;
- cursor: pointer;
-}
-.app-download-platforms img:hover{
- transform: scale(1.05);
+.app-download {
+ margin: auto auto;
+ margin-top: 100px;
+ font-size: max(3vw, 20px);
+ text-align: center;
+ font-weight: 500;
+ padding: 16px;
+ background: transparent;
+}
+.app-download-platforms {
+ display: flex;
+ justify-content: center;
+ gap: max(2vw, 10px);
+ margin-top: 40px;
+}
+/* Tiêu đề (For Better Experience Download) */
+.pforbetter, .app-download .title, .app-download h1{
+ color: var(--light-text);
+ font-weight: 700;
+ margin: 0 0 6px 0;
+}
+
+.app-download-platforms img {
+ width: max(30vw, 120px);
+ max-width: 180px;
+ transition: 0.5s;
+ cursor: pointer;
+}
+.app-download-platforms img:hover {
+ transform: scale(1.05);
}
:root .pforbetter {
- color: white;
+ color: white;
}
.lightcolors .pforbetter {
- color: black;
+ color: black;
+}
+
+/* Badge store (nút App Store/Play Store) */
+.app-download .badges{
+ display: inline-flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ margin-top: 10px;
+}
+.app-download .badges img{
+ width: auto; height: auto;
+ display: block;
+ transition: transform .08s ease, filter .2s ease;
+}
+.app-download .badges img:hover{
+ transform: translateY(-1px);
+ filter: brightness(1.03);
}
\ No newline at end of file
diff --git a/frontend/src/components/AppDownload/AppDownload.jsx b/frontend/src/components/AppDownload/AppDownload.jsx
index 0942166..c59f87b 100644
--- a/frontend/src/components/AppDownload/AppDownload.jsx
+++ b/frontend/src/components/AppDownload/AppDownload.jsx
@@ -8,7 +8,7 @@ const AppDownload = () => {
For Better Experience Download{" "}
Hangry App
-
+
{/* Google Play */}
{
+const ExploreMenu = ({
+ category,
+ setCategory,
+ cities = [],
+ restaurants = [],
+ selectedCity,
+ setSelectedCity,
+ selectedRestaurant,
+ setSelectedRestaurant,
+ lang = "vi",
+}) => {
+ const onPick = (name) => {
+ setCategory((prev) => (prev === name ? 'All' : name))
+ }
+
+ const t = (vi, en) => (lang === "vi" ? vi : en)
+ const catLabel = (name) => {
+ const map = {
+ "Salad": "Salad",
+ "Rolls": "Cuốn",
+ "Deserts": "Tráng Miệng",
+ "Sandwich": "Bánh Mì",
+ "Cake": "Bánh Ngọt",
+ "Pure Veg": "Món Chay",
+ "Pasta": "Pasta",
+ "Noodles": "Mì/Phở"
+ };
+ if (lang === "vi") return map[name] || name;
+ return name;
+ }
+
+ const renderCityName = (c) => {
+ if (c._id === "all") return t("Tất cả thành phố", "All cities");
+ return lang === "vi" ? c.name : (c.nameEn || c.name);
+ }
+ const renderRestName = (r) => {
+ if (r._id === "all") return t("Tất cả nhà hàng", "All restaurants");
+ return lang === "vi" ? r.name : (r.nameEn || r.name);
+ }
+
return (
-