diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15cc4ca..204463c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,11 @@ name: Build and Push Images on: - # push: - # # branches: - # # - main - # tags: - # - 'v*.*.*' + push: + branches: + - main + tags: + - 'v*.*.*' workflow_dispatch: jobs: @@ -38,3 +38,6 @@ jobs: *.labels.org.opencontainers.image.revision=${{ github.sha }} *.cache-from=type=gha *.cache-to=type=gha,mode=max + env: + STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }} + VITE_MAPBOX_API_KEY: ${{ secrets.VITE_MAPBOX_API_KEY }} \ No newline at end of file diff --git a/Makefile b/Makefile index ee57115..21cd2ad 100644 --- a/Makefile +++ b/Makefile @@ -34,4 +34,7 @@ kustomize-view: kubectl kustomize k8s/kustomization/base kustomize-apply: - kubectl apply -k k8s/kustomization/base \ No newline at end of file + kubectl apply -k k8s/kustomization/base + +kustomize-delete: + kubectl delete -k k8s/kustomization/base diff --git a/api-gateway/nginx.conf b/api-gateway/nginx.conf index 1600420..70e1a60 100644 --- a/api-gateway/nginx.conf +++ b/api-gateway/nginx.conf @@ -12,6 +12,14 @@ http { server notification-service:3000; } + upstream order_service { + server order-service:5000; + } + + upstream payment_service { + server payment-service:5000; + } + server { listen 5000; server_name localhost; @@ -42,6 +50,32 @@ http { add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; } + # Order Service + location /api/orders/ { + rewrite ^/api/orders/(.*)$ /orders/$1 break; + proxy_pass http://order_service/; + limit_req zone=api burst=10; + + # CORS headers + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + } + + # Payment Service + location /api/payments/ { + rewrite ^/api/payments/(.*)$ /orders/$1 break; + proxy_pass http://payment__service/; + limit_req zone=api burst=10; + + # CORS headers + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + } + location = /unauthorized { return 401; } diff --git a/docker-bake.hcl b/docker-bake.hcl index 3542bfd..b2d5c28 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -12,6 +12,9 @@ variable "TAG" { default = "latest" } +variable "STRIPE_SECRET_KEY" {} +variable "VITE_MAPBOX_API_KEY" {} + # Define common build configuration target "common" { args = { @@ -28,7 +31,7 @@ target "common" { # Default group builds all services with their specified targets from docker-compose group "default" { - targets = ["frontend", "user-service", "notification-service", "email-service", "sms-service"] + targets = ["frontend", "user-service", "notification-service", "email-service", "sms-service", "order-service", "payment-service"] } # Frontend service @@ -36,6 +39,10 @@ target "frontend" { inherits = ["common"] context = "./frontend" tags = ["${REGISTRY}/frontend:${TAG}"] + args = { + STRIPE_SECRET_KEY = "${STRIPE_SECRET_KEY}" + VITE_MAPBOX_API_KEY = "${VITE_MAPBOX_API_KEY}" + } } # User Service (default to development as in docker-compose) @@ -69,3 +76,19 @@ target "sms-service" { target = "production" tags = ["${REGISTRY}/sms-service:${TAG}"] } + +# Order Service +target "order-service" { + inherits = ["common"] + context = "./order-service" + target = "production" + tags = ["${REGISTRY}/order-service:${TAG}"] +} + +# Payment Service +target "payment-service" { + inherits = ["common"] + context = "./payment-service" + target = "production" + tags = ["${REGISTRY}/payment-service:${TAG}"] +} diff --git a/docker-compose.yml b/docker-compose.yml index 519fa0d..48a3c90 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -199,6 +199,30 @@ services: networks: - cravedrop-network + order-service: + image: nmdra/order-service + build: + context: ./order-service + target: production + container_name: order-service + hostname: order-service + ports: + - "3007:5000" + env_file: + - ./order-service/.env + + payment-service: + image: nmdra/payment-service + build: + context: ./payment-service + target: production + container_name: payment-service + hostname: payment-service + ports: + - "3008:5000" + env_file: + - ./payment-service/.env + volumes: pg_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index b891af3..7179f8e 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,5 +1,11 @@ FROM node:22-alpine AS builder +ARG STRIPE_SECRET_KEY +ARG VITE_MAPBOX_API_KEY + +ENV STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} +ENV VITE_MAPBOX_API_KEY=${VITE_MAPBOX_API_KEY}} + WORKDIR /app COPY package.json yarn.lock ./ diff --git a/frontend/package.json b/frontend/package.json index 585bba4..022ed23 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,9 +11,12 @@ "preview": "vite preview" }, "dependencies": { + "@stripe/react-stripe-js": "^3.6.0", + "@stripe/stripe-js": "^7.2.0", "@tailwindcss/vite": "^4.1.4", - "axios": "^1.8.4", + "axios": "^1.9.0", "axios-mock-adapter": "^2.1.0", + "cors": "^2.8.5", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hot-toast": "^2.5.2", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 543c1c0..c00d969 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -20,6 +20,25 @@ import Notifications from './Pages/Customer/Notifications' import SidebarLayout from './Layouts/SidebarLayout' import OrderSummary from './Pages/Customer/OrderSummary' +//order part +import { CartProvider } from './Context/CartContext' +import Home from './Pages/order/HomePage' +import ProductDetails from './Pages/order/ProductDetails' +import Cart from './Pages/order/CartPage' +import Checkout from './Pages/order/Checkoutpage' +import Payment from './Pages/order/PaymentPage' +import SuccessPage from './Pages/order/SuccessPage' + +//Restuarant part +import RestuarantRegister from "./pages/Register.jsx"; +import RestuarantLogin from "./pages/Login.jsx"; +import RestuarantDashboard from "./pages/Dashboard.jsx"; +import RestuarantMenuManagement from './pages/MenuManagement.jsx'; +import RestuarantProfileSettings from "./pages/ProfileSettings.jsx"; +import RestuarantAdminDashboard from "./pages/AdminDashboard.jsx"; +import "./restaurnat.css"; +import Restuarantstyles from "./App.module.css"; + const router = createBrowserRouter( createRoutesFromElements( <> @@ -37,6 +56,23 @@ const router = createBrowserRouter( } /> + {/* order part */} + } /> + } /> + } /> + } /> + } /> + } /> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Catch-all for 404 */} } /> @@ -46,10 +82,10 @@ const router = createBrowserRouter( const App = () => { return ( - <> + - + ) } diff --git a/frontend/src/Components/Home/ShopList.jsx b/frontend/src/Components/Home/ShopList.jsx index 1088c1a..458cfb7 100644 --- a/frontend/src/Components/Home/ShopList.jsx +++ b/frontend/src/Components/Home/ShopList.jsx @@ -97,7 +97,7 @@ const FoodList = () => {
diff --git a/frontend/src/Components/order/ProductCard.jsx b/frontend/src/Components/order/ProductCard.jsx new file mode 100644 index 0000000..2b32f5b --- /dev/null +++ b/frontend/src/Components/order/ProductCard.jsx @@ -0,0 +1,30 @@ +import React from 'react'; + +const ProductCard = ({ product }) => { + return ( +
+
+ {product.name} +
+ New +
+
+
+

{product.name}

+

{product.description}

+
+ ${product.price.toFixed(2)} + +
+
+
+ ); +}; + +export default ProductCard; \ No newline at end of file diff --git a/frontend/src/Context/CartContext.jsx b/frontend/src/Context/CartContext.jsx new file mode 100644 index 0000000..2d823aa --- /dev/null +++ b/frontend/src/Context/CartContext.jsx @@ -0,0 +1,20 @@ +import { createContext, useState, useEffect } from 'react'; + +export const CartContext = createContext(); + +export const CartProvider = ({ children }) => { + const [cartItems, setCartItems] = useState(() => { + const storedCart = localStorage.getItem('cartItems'); + return storedCart ? JSON.parse(storedCart) : []; + }); + + useEffect(() => { + localStorage.setItem('cartItems', JSON.stringify(cartItems)); + }, [cartItems]); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/Pages/Restaurant/AdminDashboard.jsx b/frontend/src/Pages/Restaurant/AdminDashboard.jsx new file mode 100644 index 0000000..69d5f3d --- /dev/null +++ b/frontend/src/Pages/Restaurant/AdminDashboard.jsx @@ -0,0 +1,183 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from "../services/axios"; +import styles from './AdminDashboard.module.css'; + +const AdminDashboard = () => { + const navigate = useNavigate(); + const [restaurants, setRestaurants] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [message, setMessage] = useState(''); + + // Check admin authentication + useEffect(() => { + const adminToken = localStorage.getItem('adminToken'); + if (!adminToken) { + navigate('/login'); + return; + } + + // Load restaurants data + fetchRestaurants(); + }, [navigate]); + + const fetchRestaurants = async () => { + try { + setLoading(true); + const response = await axios.get('/api/admin/restaurants', { + headers: { + Authorization: `Bearer ${localStorage.getItem('adminToken')}` + } + }); + + setRestaurants(response.data); + setError(null); + } catch (err) { + console.error('Error fetching restaurants:', err); + setError('Failed to load restaurants. Please try again.'); + } finally { + setLoading(false); + } + }; + + const handleVerifyRestaurant = async (restaurantId) => { + try { + await axios.put(`/api/admin/verify/${restaurantId}`, {}, { + headers: { + Authorization: `Bearer ${localStorage.getItem('adminToken')}` + } + }); + + setMessage('Restaurant verified successfully'); + + // Update the restaurants list to reflect the change + setRestaurants(prevRestaurants => + prevRestaurants.map(restaurant => + restaurant._id === restaurantId + ? { ...restaurant, isVerified: true } + : restaurant + ) + ); + } catch (err) { + console.error('Error verifying restaurant:', err); + setMessage('Failed to verify restaurant. Please try again.'); + } + }; + + const handleDeleteRestaurant = async (restaurantId) => { + if (!window.confirm('Are you sure you want to delete this restaurant? This action cannot be undone.')) { + return; + } + + try { + // Use the admin route for deletion + await axios.delete(`/api/admin/restaurants/${restaurantId}`, { + headers: { + Authorization: `Bearer ${localStorage.getItem('adminToken')}` + } + }); + + setMessage('Restaurant deleted successfully'); + + // Remove the deleted restaurant from the list + setRestaurants(prevRestaurants => + prevRestaurants.filter(restaurant => restaurant._id !== restaurantId) + ); + } catch (err) { + console.error('Error deleting restaurant:', err); + setMessage('Failed to delete restaurant. Please try again.'); + } + }; + + const handleLogout = () => { + localStorage.removeItem('adminToken'); + localStorage.removeItem('adminUser'); + navigate('/login'); + }; + + if (loading) { + return ( +
+
+
+

Loading restaurants...

+
+
+ ); + } + + return ( +
+
+

Admin Dashboard

+ +
+ + {message && ( +
+ {message} + +
+ )} + +
+

Restaurant Management

+ + {error &&

{error}

} + + {restaurants.length === 0 ? ( +

No restaurants found.

+ ) : ( +
+ + + + + + + + + + + + {restaurants.map(restaurant => ( + + + + + + + + ))} + +
NameEmailPhoneStatusActions
{restaurant.name}{restaurant.email}{restaurant.telephoneNumber} + + {restaurant.isVerified ? 'Verified' : 'Pending'} + + + {!restaurant.isVerified && ( + + )} + +
+
+ )} +
+
+ ); +}; + +export default AdminDashboard; \ No newline at end of file diff --git a/frontend/src/Pages/Restaurant/AdminDashboard.module.css b/frontend/src/Pages/Restaurant/AdminDashboard.module.css new file mode 100644 index 0000000..1642106 --- /dev/null +++ b/frontend/src/Pages/Restaurant/AdminDashboard.module.css @@ -0,0 +1,189 @@ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.header h1 { + font-size: 1.8rem; + color: var(--text-color); + margin: 0; +} + +.logoutButton { + background-color: transparent; + border: 1px solid var(--border-color); + color: var(--text-color); + padding: 0.5rem 1rem; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; +} + +.logoutButton:hover { + background-color: var(--hover-background); + color: var(--error-color); + border-color: var(--error-color); +} + +.content { + background: var(--card-background); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 4px 12px var(--shadow-color); +} + +.content h2 { + font-size: 1.25rem; + margin-top: 0; + margin-bottom: 1.5rem; + color: var(--text-color); +} + +.message { + padding: 1rem; + border-radius: 6px; + margin-bottom: 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.success { + background-color: rgba(var(--success-color-rgb), 0.1); + color: var(--success-color); +} + +.error { + background-color: rgba(var(--error-color-rgb), 0.1); + color: var(--error-color); +} + +.closeMessage { + background: transparent; + border: none; + font-size: 1.25rem; + cursor: pointer; + color: inherit; +} + +.noData { + text-align: center; + color: var(--text-secondary); + padding: 2rem; +} + +.restaurantsTable { + overflow-x: auto; +} + +.restaurantsTable table { + width: 100%; + border-collapse: collapse; +} + +.restaurantsTable th { + text-align: left; + padding: 1rem; + background-color: var(--hover-background); + color: var(--text-color); + font-weight: 600; +} + +.restaurantsTable td { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + color: var(--text-color); +} + +.statusBadge { + display: inline-block; + padding: 0.35rem 0.75rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 600; +} + +.verified { + background-color: rgba(var(--success-color-rgb), 0.1); + color: var(--success-color); +} + +.pending { + background-color: rgba(var(--primary-color-rgb), 0.1); + color: var(--primary-color); +} + +.actions { + display: flex; + gap: 0.5rem; +} + +.verifyButton, .deleteButton { + padding: 0.5rem 0.75rem; + border-radius: 4px; + border: none; + font-weight: 500; + cursor: pointer; +} + +.verifyButton { + background-color: var(--primary-color); + color: white; +} + +.verifyButton:hover { + background-color: var(--secondary-color); +} + +.deleteButton { + background-color: var(--error-color); + color: white; +} + +.deleteButton:hover { + filter: brightness(90%); +} + +.loadingContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; +} + +.spinner { + border: 4px solid rgba(0, 0, 0, 0.1); + border-radius: 50%; + border-top: 4px solid var(--primary-color); + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin-bottom: 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .actions { + flex-direction: column; + } +} \ No newline at end of file diff --git a/frontend/src/Pages/Restaurant/Dashboard.jsx b/frontend/src/Pages/Restaurant/Dashboard.jsx new file mode 100644 index 0000000..3d228a4 --- /dev/null +++ b/frontend/src/Pages/Restaurant/Dashboard.jsx @@ -0,0 +1,372 @@ +import React, { useState, useEffect } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import axios from "../services/axios"; +import styles from './Dashboard.module.css'; + +const Dashboard = () => { + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [restaurantData, setRestaurantData] = useState(null); + const [menuItems, setMenuItems] = useState([]); + const [stats, setStats] = useState({ + menuCount: 0, + isVerified: false + }); + + // Check authentication first + useEffect(() => { + const token = localStorage.getItem('token'); + if (!token) { + navigate('/login'); + return; + } + + // Load restaurant and menu data + loadDashboardData(); + }, [navigate]); + + const loadDashboardData = async () => { + try { + setIsLoading(true); + + // Get restaurant data from localStorage + const storedRestaurant = JSON.parse(localStorage.getItem('restaurant')); + + if (!storedRestaurant || !storedRestaurant.id) { + throw new Error("Restaurant information not found"); + } + + // Fetch restaurant details + const restaurantResponse = await axios.get(`/api/restaurants/${storedRestaurant.id}`, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}` + } + }); + + // Fetch menu items + const menuResponse = await axios.get(`/api/restaurants/menu/${storedRestaurant.id}`, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}` + } + }); + + setRestaurantData(restaurantResponse.data); + setMenuItems(menuResponse.data.menu || []); + setStats({ + menuCount: menuResponse.data.menu?.length || 0, + isVerified: restaurantResponse.data.isVerified + }); + + // Update localStorage with latest restaurant data + localStorage.setItem('restaurant', JSON.stringify({ + ...storedRestaurant, + _id: restaurantResponse.data._id, + name: restaurantResponse.data.name, + isOpen: restaurantResponse.data.isOpen, + isVerified: restaurantResponse.data.isVerified, + logo: restaurantResponse.data.logo, + coverImage: restaurantResponse.data.coverImage + })); + + } catch (err) { + console.error("Dashboard initialization error:", err); + setError("Failed to load restaurant data"); + } finally { + setIsLoading(false); + } + }; + + // Handle restaurant status toggle + const handleToggleStatus = async () => { + try { + const newStatus = !restaurantData.isOpen; + + // Call API to update restaurant status + await axios.post("/api/restaurants/availability", { + restaurantId: restaurantData._id, + isOpen: newStatus + }, { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}` + } + }); + + // Update local state + setRestaurantData({ + ...restaurantData, + isOpen: newStatus + }); + + // Update localStorage + const storedRestaurant = JSON.parse(localStorage.getItem('restaurant')); + localStorage.setItem('restaurant', JSON.stringify({ + ...storedRestaurant, + isOpen: newStatus + })); + + } catch (err) { + console.error("Error toggling restaurant status:", err); + } + }; + + // Get user initials for avatar + const getUserInitials = () => { + if (!restaurantData || !restaurantData.name) return 'R'; + return restaurantData.name.charAt(0).toUpperCase(); + }; + + if (isLoading) { + return ( +
+
+
+

Loading your restaurant dashboard...

+
+
+ ); + } + + // Show a basic error state + if (error) { + return ( +
+
+

Something went wrong

+

{error}

+ +
+
+ ); + } + + return ( +
+ {/* Cover Photo Banner */} +
+ {restaurantData?.coverImage ? ( + {`${restaurantData.name} { + e.target.onerror = null; + e.target.src = "https://via.placeholder.com/1200x300?text=No+Cover+Photo"; + }} + /> + ) : ( +
+ Add a cover photo to enhance your restaurant profile +
+ )} + + ✏️ Edit Cover + +
+ + {/* Header */} +
+
+
🍽️
+

Restaurant Dashboard

+
+
+ +
+
+ + {/* Profile Section */} +
+ {restaurantData?.logo ? ( + {restaurantData.name} { + e.target.onerror = null; + e.target.style.display = "none"; + const fallback = document.createElement("div"); + fallback.className = styles.userAvatar; + fallback.textContent = getUserInitials(); + e.target.parentNode.appendChild(fallback); + }} + /> + ) : ( +
+ {getUserInitials()} +
+ )} +
+
+

Welcome back, {restaurantData?.name || 'Restaurant'}!

+
+ {restaurantData?.isVerified ? + ✓ Verified Account : + Pending Verification + } +
+
+

Manage your restaurant, update menu items, and track your business from here.

+
+
+ + {/* Dashboard Cards */} +
+ {/* Restaurant Status Card */} +
+
+

Restaurant Status

+
+
+
+
+ {restaurantData?.isOpen ? 'Currently Open' : 'Currently Closed'} +
+

+ {restaurantData?.isOpen + ? 'Your restaurant is visible to customers and accepting orders.' + : 'Your restaurant is hidden from customers and not accepting orders.'} +

+
+ +
+
+ + {/* Menu Stats Card */} +
+
+

Menu Items

+
+
+
+
{stats.menuCount}
+

Total Items

+
+ + Manage Menu → + + {menuItems.length === 0 && ( +
+ You haven't added any menu items yet. Start building your menu! +
+ )} +
+
+ + {/* Location Card */} +
+
+

Location

+
+
+
+ {restaurantData?.location ? ( + <> +

+ {restaurantData?.location?.address || ''} +

+

+ {restaurantData?.location?.city || ''}{restaurantData?.location?.postalCode ? `, ${restaurantData.location.postalCode}` : ''} +

+ + ) : ( +

No location information available

+ )} +
+ + Update Location → + +
+
+ + {/* Appearance Card */} +
+
+

Appearance

+
+
+
+ Restaurant Logo/Cover Image +
+ + Update Appearance → + +
+
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ +
📋
+
+

Menu Management

+

Add, edit, or remove menu items

+
+ + +
⚙️
+
+

Profile Settings

+

Update your restaurant details

+
+ +
+
+ + {/* Recent Menu Preview (if menu items exist) */} + {menuItems.length > 0 && ( +
+
+

Recent Menu Items

+ View All +
+
+ {menuItems.slice(0, 3).map((item) => ( +
+ {item.image ? ( +
+ {item.name} { + e.target.onerror = null; + e.target.src = "https://via.placeholder.com/80?text=Food"; + }} + /> +
+ ) : ( +
+ No Image +
+ )} +
+

{item.name}

+

${parseFloat(item.price).toFixed(2)}

+
+
+ ))} +
+
+ )} +
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/frontend/src/Pages/Restaurant/Dashboard.module.css b/frontend/src/Pages/Restaurant/Dashboard.module.css new file mode 100644 index 0000000..b6c5e6d --- /dev/null +++ b/frontend/src/Pages/Restaurant/Dashboard.module.css @@ -0,0 +1,589 @@ +/* Base animations */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Main container */ +.dashboardContainer { + background-color: var(--background-color); + max-width: 1400px; + margin: 0 auto; + padding: 2rem; + color: var(--text-color); + min-height: 100vh; +} + +/* Header styles */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.headerTitle { + display: flex; + align-items: center; +} + +.headerTitle h1 { + font-size: 1.8rem; + font-weight: 700; + margin: 0; +} + +.headerTitle span { + color: var(--primary-color); +} + +.logo { + font-size: 1.8rem; + margin-right: 0.75rem; +} + +/* Logout button */ +.logoutButton { + background-color: transparent; + border: 1px solid var(--border-color); + color: var(--text-color); + padding: 0.5rem 1rem; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; +} + +.logoutButton:hover { + background-color: var(--hover-background); + color: var(--error-color); + border-color: var(--error-color); +} + +/* Profile section */ +.profileSection { + display: flex; + align-items: center; + gap: 1.5rem; + background: var(--card-background); + padding: 1.5rem; + border-radius: 12px; + margin-bottom: 2rem; + box-shadow: 0 2px 8px var(--shadow-color); + animation: fadeIn 0.5s ease; +} + +.userAvatar { + width: 80px; + height: 80px; + border-radius: 50%; + background: var(--primary-color); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 700; + font-size: 2rem; + flex-shrink: 0; +} + +.welcomeInfo { + flex: 1; +} + +.welcomeHeader { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 0.5rem; +} + +.welcomeInfo h2 { + font-size: 1.5rem; + margin: 0; + color: var(--text-color); +} + +.welcomeInfo p { + color: var(--text-secondary); + margin: 0; +} + +.verificationBadge { + display: inline-block; +} + +.verified, .pending { + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 600; +} + +.verified { + background-color: rgba(var(--success-color-rgb), 0.1); + color: var(--success-color); +} + +.pending { + background-color: rgba(var(--primary-color-rgb), 0.1); + color: var(--primary-color); +} + +/* Dashboard grid */ +.dashboardGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 2.5rem; +} + +/* Cards */ +.dashboardCard { + background: var(--card-background); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px var(--shadow-color); + transition: transform 0.3s ease, box-shadow 0.3s ease; + animation: fadeIn 0.5s ease; +} + +.dashboardCard:hover { + transform: translateY(-5px); + box-shadow: 0 8px 16px var(--shadow-color); +} + +.cardHeader { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); +} + +.cardHeader h3 { + margin: 0; + font-size: 1.1rem; + font-weight: 600; +} + +.cardContent { + padding: 1.5rem; +} + +/* Status styles */ +.statusInfo { + margin-bottom: 1.5rem; +} + +.statusBadge { + display: inline-block; + padding: 0.5rem 1rem; + border-radius: 6px; + font-weight: 600; + margin-bottom: 1rem; + font-size: 1rem; +} + +.statusOpen { + background-color: rgba(var(--success-color-rgb), 0.1); + color: var(--success-color); +} + +.statusClosed { + background-color: rgba(var(--error-color-rgb), 0.1); + color: var(--error-color); +} + +.statusHint { + color: var(--text-secondary); + margin: 0; + font-size: 0.9rem; +} + +.statusToggleButton { + width: 100%; + padding: 0.8rem; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + border: none; + transition: all 0.2s ease; +} + +.openButton { + background-color: rgba(var(--success-color-rgb), 0.1); + color: var(--success-color); +} + +.openButton:hover { + background-color: rgba(var(--success-color-rgb), 0.2); +} + +.closeButton { + background-color: rgba(var(--error-color-rgb), 0.1); + color: var(--error-color); +} + +.closeButton:hover { + background-color: rgba(var(--error-color-rgb), 0.2); +} + +/* Menu stats */ +.menuStats { + margin-bottom: 1.5rem; + text-align: center; +} + +.statCount { + font-size: 3rem; + font-weight: 700; + color: var(--primary-color); +} + +.statLabel { + color: var(--text-secondary); + margin: 0; +} + +.emptyMenuHint { + margin-top: 1rem; + padding: 0.75rem; + background: rgba(var(--primary-color-rgb), 0.05); + border-radius: 6px; + color: var(--text-secondary); + font-size: 0.9rem; + text-align: center; +} + +/* Location */ +.locationInfo { + margin-bottom: 1.5rem; +} + +.address, .cityInfo { + margin: 0.25rem 0; +} + +.noInfo { + color: var(--text-secondary); + font-style: italic; +} + +/* Cuisine tags */ +.cuisineTags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +.cuisineTag { + background: var(--tag-background); + color: var(--tag-text); + padding: 0.35rem 0.75rem; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 500; +} + +.noCuisine { + color: var(--text-secondary); + font-style: italic; +} + +/* Action links */ +.actionLink { + display: inline-block; + color: var(--primary-color); + text-decoration: none; + font-weight: 600; + transition: all 0.2s ease; +} + +.actionLink:hover { + color: var(--secondary-color); + transform: translateX(3px); +} + +/* Quick actions section */ +.quickActionsSection { + margin-bottom: 2.5rem; +} + +.sectionTitle { + margin-bottom: 1.5rem; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-color); +} + +.quickActionGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1.5rem; +} + +.quickActionCard { + display: flex; + align-items: center; + gap: 1rem; + background: var(--card-background); + padding: 1.5rem; + border-radius: 12px; + text-decoration: none; + box-shadow: 0 2px 8px var(--shadow-color); + transition: all 0.3s ease; +} + +.quickActionCard:hover { + transform: translateY(-5px); + box-shadow: 0 8px 16px var(--shadow-color); +} + +.quickActionIcon { + font-size: 2rem; +} + +.quickActionText { + flex: 1; +} + +.quickActionText h4 { + margin: 0 0 0.5rem 0; + color: var(--text-color); +} + +.quickActionText p { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; +} + +/* Recent menu section */ +.recentMenuSection { + background: var(--card-background); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 8px var(--shadow-color); + animation: fadeIn 0.5s ease; +} + +.sectionHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.sectionHeader h3 { + margin: 0; +} + +.viewAllLink { + color: var(--primary-color); + text-decoration: none; + font-weight: 600; + transition: all 0.2s ease; +} + +.viewAllLink:hover { + color: var(--secondary-color); +} + +.recentMenuGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1.5rem; +} + +.menuItemCard { + border-radius: 8px; + overflow: hidden; + background: var(--hover-background); + transition: all 0.3s ease; +} + +.menuItemCard:hover { + transform: translateY(-5px); + box-shadow: 0 5px 15px var(--shadow-color); +} + +.menuItemImage { + height: 120px; + overflow: hidden; +} + +.menuItemImage img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.menuItemImagePlaceholder { + height: 120px; + display: flex; + align-items: center; + justify-content: center; + background: var(--input-background); + color: var(--text-secondary); +} + +.menuItemInfo { + padding: 1rem; +} + +.menuItemInfo h4 { + margin: 0 0 0.5rem 0; + font-size: 1rem; + color: var(--text-color); +} + +.menuItemPrice { + margin: 0; + color: var(--primary-color); + font-weight: 600; +} + +/* Loading and error states */ +.loadingContainer, .errorMessage { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; + text-align: center; +} + +.spinner { + width: 50px; + height: 50px; + border: 4px solid rgba(var(--primary-color-rgb), 0.1); + border-left-color: var(--primary-color); + border-radius: 50%; + animation: rotate 1s linear infinite; + margin-bottom: 1.5rem; +} + +.errorMessage h2 { + margin-bottom: 1rem; + color: var(--error-color); +} + +.errorMessage button { + padding: 0.8rem 1.5rem; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + margin-top: 1.5rem; + transition: all 0.2s ease; +} + +.errorMessage button:hover { + background-color: var(--secondary-color); +} + +/* Cover Photo Styles */ +.coverPhotoContainer { + position: relative; + width: 100%; + height: 200px; + margin-bottom: 2rem; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 12px var(--shadow-color); +} + +.coverPhoto { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.coverPhotoPlaceholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, var(--hover-background) 0%, var(--card-background) 100%); + color: var(--text-secondary); + font-size: 1.1rem; + text-align: center; + padding: 1rem; +} + +.editCoverButton { + position: absolute; + bottom: 15px; + right: 15px; + background: rgba(0, 0, 0, 0.6); + color: white; + padding: 0.5rem 1rem; + border-radius: 20px; + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.5rem; + opacity: 0.8; + transition: all 0.2s ease; +} + +.editIcon { + font-size: 1.1rem; +} + +.editCoverButton:hover { + opacity: 1; + transform: translateY(-2px); +} + +.coverPhotoContainer:hover .coverPhoto { + transform: scale(1.05); +} + +@media (max-width: 768px) { + .dashboardContainer { + padding: 1rem; + } + + .profileSection { + flex-direction: column; + text-align: center; + } + + .welcomeHeader { + flex-direction: column; + align-items: center; + } + + .dashboardGrid, .quickActionGrid, .recentMenuGrid { + grid-template-columns: 1fr; + } + + .coverPhotoContainer { + height: 150px; + margin-bottom: 1.5rem; + } + + .editCoverButton { + padding: 0.4rem 0.8rem; + font-size: 0.8rem; + } +} \ No newline at end of file diff --git a/frontend/src/Pages/Restaurant/Login.jsx b/frontend/src/Pages/Restaurant/Login.jsx new file mode 100644 index 0000000..452eeb8 --- /dev/null +++ b/frontend/src/Pages/Restaurant/Login.jsx @@ -0,0 +1,304 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Link, useNavigate, useLocation } from "react-router-dom"; +import axios from "../services/axios"; +import styles from './Login.module.css'; + +const Login = ({ darkMode }) => { + const navigate = useNavigate(); + const location = useLocation(); + const emailInputRef = useRef(null); + + const [formData, setFormData] = useState({ + email: "", + password: "", + }); + + const [errors, setErrors] = useState({}); + const [touched, setTouched] = useState({}); + const [message, setMessage] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [loginAttempts, setLoginAttempts] = useState(0); + const [showPassword, setShowPassword] = useState(false); + // Add this state to track if admin login is selected + const [isAdminLogin, setIsAdminLogin] = useState(false); + + // Focus email input on component mount + useEffect(() => { + if (emailInputRef.current) { + emailInputRef.current.focus(); + } + }, []); + + // Check for message in location state (e.g., from successful registration or account deletion) + useEffect(() => { + if (location.state?.message) { + setMessage(location.state.message); + // Clear the location state + navigate(location.pathname, { replace: true, state: {} }); + } + }, [location, navigate]); + + const validateField = (name, value) => { + let error = ""; + + switch(name) { + case "email": + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!value.trim()) { + error = "Email address is required"; + } else if (!emailRegex.test(value)) { + error = "Please enter a valid email address"; + } + break; + + case "password": + if (!value) { + error = "Password is required"; + } + break; + + default: + break; + } + + return error; + }; + + const validateForm = () => { + const newErrors = {}; + + // Validate all fields + for (const field in formData) { + const error = validateField(field, formData[field]); + if (error) newErrors[field] = error; + } + + // Additional check for repeated login attempts + if (loginAttempts >= 3) { + newErrors.general = "Multiple failed login attempts detected. Please verify your credentials or reset your password."; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleBlur = (e) => { + const { name, value } = e.target; + setTouched(prev => ({ ...prev, [name]: true })); + + const error = validateField(name, value); + setErrors(prev => ({ ...prev, [name]: error })); + }; + + const handleChange = (e) => { + const { name, value } = e.target; + + // Clear errors when typing + if (touched[name]) { + const error = validateField(name, value); + setErrors(prev => ({ ...prev, [name]: error, general: '' })); + } + + setFormData({ ...formData, [name]: value }); + }; + + // Add this function to handle the toggle between user and admin login + const toggleLoginType = () => { + setIsAdminLogin(!isAdminLogin); + // Clear any existing errors when switching login types + setErrors({}); + setMessage(""); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + // Clear any existing messages + setMessage(""); + + // Mark all fields as touched + setTouched({ + email: true, + password: true + }); + + // Validate form + if (!validateForm()) { + return; + } + + setIsLoading(true); + + try { + // Different endpoint for admin login + const endpoint = isAdminLogin ? "/api/auth/admin/login" : "/api/auth/login"; + const response = await axios.post(endpoint, formData); + + // Reset login attempts on successful login + setLoginAttempts(0); + + if (isAdminLogin) { + // Store admin token and info differently to distinguish from restaurant users + localStorage.setItem("adminToken", response.data.token); + localStorage.setItem("adminUser", JSON.stringify(response.data.user)); + + setMessage("Admin login successful! Redirecting..."); + + // Redirect to admin dashboard + setTimeout(() => { + navigate("/admin/dashboard"); + }, 1500); + } else { + // Regular restaurant login (your existing code) + localStorage.setItem("token", response.data.token); + localStorage.setItem("restaurant", JSON.stringify(response.data.restaurant)); + + setMessage("Login successful! Redirecting..."); + + // Redirect to restaurant dashboard + setTimeout(() => { + navigate("/dashboard"); + }, 1500); + } + } catch (error) { + // Increment login attempts on failure + setLoginAttempts(prev => prev + 1); + + // Handle error messages + if (error.response?.status === 401) { + setMessage("Invalid credentials. Please check your email and password."); + } else if (error.response?.status === 429) { + setMessage("Too many login attempts. Please try again later."); + } else { + setMessage(error.response?.data?.message || "Login failed. Please try again."); + } + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+ + +
+ +

{isAdminLogin ? "Admin Login" : "Restaurant Login"}

+ + {errors.general && ( +
+ {errors.general} +
+ )} + +
+ + + {touched.email && errors.email && ( + + )} +
+ +
+ +
+ + +
+ {touched.password && errors.password && ( + + )} +
+ +
+ Forgot password? +
+ + + + {message && ( + + )} + +

+ Don't have an account? Register here +

+
+
+ ); +}; + +export default Login; \ No newline at end of file diff --git a/frontend/src/Pages/Restaurant/Login.module.css b/frontend/src/Pages/Restaurant/Login.module.css new file mode 100644 index 0000000..7d34455 --- /dev/null +++ b/frontend/src/Pages/Restaurant/Login.module.css @@ -0,0 +1,259 @@ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.container { + min-height: 100vh; + background: var(--background-color); + padding: 2rem; + display: flex; + justify-content: center; + align-items: center; +} + +.form { + background: var(--card-background); + width: 100%; + max-width: 500px; + margin: 0 auto; + padding: 2.5rem; + border-radius: 15px; + box-shadow: 0 10px 30px var(--shadow-color); + animation: fadeIn 0.6s ease-out; +} + +.title { + color: var(--text-color); + text-align: center; + margin-bottom: 2.5rem; + font-size: 2.5rem; + font-weight: 700; + position: relative; +} + +.title:after { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 60px; + height: 4px; + background: var(--primary-color); +} + +.inputGroup { + margin-bottom: 1.8rem; +} + +.label { + display: block; + margin-bottom: 0.6rem; + color: var(--text-color); + font-weight: 600; + font-size: 0.95rem; + transition: color 0.3s ease; +} + +.input { + width: 100%; + padding: 0.9rem; + border: 2px solid var(--border-color); + border-radius: 8px; + font-size: 1rem; + transition: all 0.3s ease; + background: var(--input-background); + color: var(--text-color); +} + +.input:hover { + border-color: var(--text-secondary); +} + +.input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.3); + background: var(--card-background); +} + +.button { + width: 100%; + padding: 1rem; + background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + color: var(--button-text); + border: none; + border-radius: 8px; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + margin-top: 1rem; + position: relative; + overflow: hidden; +} + +.button:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px var(--shadow-color); +} + +.button:active { + transform: translateY(0); +} + +.error { + color: var(--error-color); + text-align: center; + margin-top: 1rem; + padding: 0.8rem; + border-radius: 8px; + background: rgba(231, 76, 60, 0.1); + border: 1px solid rgba(231, 76, 60, 0.2); + animation: fadeIn 0.3s ease-out; +} + +.success { + color: var(--success-color); + text-align: center; + margin-top: 1rem; + padding: 0.8rem; + border-radius: 8px; + background: rgba(39, 174, 96, 0.1); + border: 1px solid rgba(39, 174, 96, 0.2); + animation: fadeIn 0.3s ease-out; +} + +.buttonLoading { + composes: button; + position: relative; + color: transparent; +} + +.buttonLoading:after { + content: ''; + position: absolute; + width: 20px; + height: 20px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 50%; + border: 2px solid var(--button-text); + border-left-color: transparent; + border-top-color: var(--primary-color); + animation: spin 1s linear infinite; +} + +.registerLink { + text-align: center; + margin-top: 1.5rem; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.registerLink a { + color: var(--primary-color); + text-decoration: none; + font-weight: 600; +} + +.registerLink a:hover { + text-decoration: underline; +} + +@keyframes spin { + 0% { transform: translate(-50%, -50%) rotate(0deg); } + 100% { transform: translate(-50%, -50%) rotate(360deg); } +} + +/* Responsive design - retain existing media queries */ + +/* Add these new styles to your existing CSS file */ + +/* Required field indicator */ +.requiredStar { + color: var(--error-color); + margin-left: 0.2rem; +} + +/* Password input container */ +.passwordInputContainer { + position: relative; + display: flex; + align-items: center; +} + +.togglePasswordBtn { + position: absolute; + right: 10px; + background: none; + border: none; + color: var(--text-secondary); + font-size: 0.8rem; + cursor: pointer; + padding: 5px; +} + +.togglePasswordBtn:hover { + color: var(--primary-color); +} + +/* Forgot password link */ +.forgotPassword { + text-align: right; + margin-bottom: 1.5rem; + font-size: 0.9rem; +} + +.forgotPassword a { + color: var(--primary-color); + text-decoration: none; + transition: all 0.2s ease; +} + +.forgotPassword a:hover { + text-decoration: underline; + color: var(--secondary-color); +} + +/* Animation for error messages */ +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } + 20%, 40%, 60%, 80% { transform: translateX(5px); } +} + +.errorText { + animation: shake 0.6s cubic-bezier(.36,.07,.19,.97) both; +} + +.loginTypeToggle { + display: flex; + margin-bottom: 1.5rem; + width: 100%; + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--border-color); +} + +.loginTypeButton { + flex: 1; + padding: 0.75rem; + border: none; + background: var(--input-background); + color: var(--text-color); + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.loginTypeButton.active { + background: var(--primary-color); + color: white; +} + +.loginTypeButton:hover:not(.active) { + background: var(--hover-background); +} \ No newline at end of file diff --git a/frontend/src/Pages/Restaurant/MenuManagement.jsx b/frontend/src/Pages/Restaurant/MenuManagement.jsx new file mode 100644 index 0000000..c35b8e3 --- /dev/null +++ b/frontend/src/Pages/Restaurant/MenuManagement.jsx @@ -0,0 +1,355 @@ +// src/pages/MenuManagement.jsx +import React, { useState, useEffect } from "react"; +import axios from "../services/axios"; +import styles from "./MenuManagement.module.css"; + +const MenuManagement = () => { + const [menuItems, setMenuItems] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [editingItem, setEditingItem] = useState(null); + const [formData, setFormData] = useState({ + name: "", + description: "", + price: "", + image: null + }); + const [message, setMessage] = useState(""); + + // Get restaurant ID from localStorage + const getRestaurantId = () => { + const restaurant = JSON.parse(localStorage.getItem("restaurant")); + return restaurant?.id; + }; + + // Fetch menu items + const fetchMenuItems = async () => { + setLoading(true); + try { + const restaurantId = getRestaurantId(); + if (!restaurantId) { + throw new Error("Restaurant ID not found"); + } + + const response = await axios.get(`/api/restaurants/menu/${restaurantId}`, { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}` + } + }); + + setMenuItems(response.data.menu || []); + setError(""); + } catch (err) { + console.error("Error fetching menu items:", err); + setError("Failed to load menu items. Please try again."); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchMenuItems(); + }, []); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value + }); + }; + + const handleFileChange = (e) => { + setFormData({ + ...formData, + image: e.target.files[0] + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setMessage(""); + + try { + const restaurantId = getRestaurantId(); + if (!restaurantId) { + throw new Error("Restaurant ID not found"); + } + + // Create FormData object for file upload + const form = new FormData(); + form.append("restaurantId", restaurantId); + form.append("name", formData.name); + form.append("description", formData.description || ""); + form.append("price", formData.price); + + // Add image if it exists + if (formData.image) { + form.append("image", formData.image); + console.log("Image attached:", formData.image.name); + } + + let response; + if (editingItem) { + // Update existing item + response = await axios.put( + `/api/restaurants/menu/${restaurantId}/${editingItem._id}`, + form, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}` + // Don't set Content-Type manually - axios will set it for FormData + } + } + ); + console.log("Update response:", response.data); + setMessage("Menu item updated successfully"); + } else { + // Add new item + response = await axios.post( + "/api/restaurants/menu", + form, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}` + // Don't set Content-Type manually - axios will set it for FormData + } + } + ); + console.log("Create response:", response.data); + setMessage("Menu item added successfully"); + } + + // Reset form and fetch updated menu items + setFormData({ name: "", description: "", price: "", image: null }); + document.getElementById("image").value = ""; // Reset file input + setEditingItem(null); + fetchMenuItems(); + } catch (err) { + console.error("Error with menu item:", err); + console.error("Error details:", err.response?.data); + setMessage(err.response?.data?.message || "Failed to save menu item"); + } + }; + + const handleEdit = (item) => { + setEditingItem(item); + setFormData({ + name: item.name, + description: item.description || "", + price: item.price, + image: null // Can't pre-populate file input + }); + }; + + const handleDelete = async (itemId) => { + if (!window.confirm("Are you sure you want to delete this menu item?")) { + return; + } + + try { + const restaurantId = getRestaurantId(); + await axios.delete(`/api/restaurants/menu/${restaurantId}/${itemId}`, { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}` + } + }); + + setMessage("Menu item deleted successfully"); + fetchMenuItems(); + } catch (err) { + console.error("Error deleting menu item:", err); + setMessage("Failed to delete menu item"); + } + }; + + const handleDeleteAll = async () => { + if (!window.confirm("Are you sure you want to delete ALL menu items? This cannot be undone.")) { + return; + } + + try { + const restaurantId = getRestaurantId(); + await axios.delete(`/api/restaurants/menu/${restaurantId}/all`, { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}` + } + }); + + setMessage("All menu items deleted successfully"); + fetchMenuItems(); + } catch (err) { + console.error("Error deleting all menu items:", err); + setMessage("Failed to delete all menu items"); + } + }; + + const handleCancel = () => { + setEditingItem(null); + setFormData({ name: "", description: "", price: "", image: null }); + document.getElementById("image").value = ""; // Reset file input + }; + + // If loading + if (loading && menuItems.length === 0) { + return ( +
+
+

Loading menu items...

+
+ ); + } + + return ( +
+

Menu Management

+ + {message && ( +
+ {message} +
+ )} + +
+
+

{editingItem ? "Edit Menu Item" : "Add New Menu Item"}

+
+
+ + +
+ +
+ +