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 = () => {
navigate('/foods')}
+ onClick={() => navigate('/home')}
>
View More
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 (
+
+
+
+
+ New
+
+
+
+
{product.name}
+
{product.description}
+
+ ${product.price.toFixed(2)}
+
+ Add t Cart
+
+
+
+
+ );
+};
+
+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
+
+ Logout
+
+
+
+ {message && (
+
+ {message}
+ setMessage('')}>×
+
+ )}
+
+
+
Restaurant Management
+
+ {error &&
{error}
}
+
+ {restaurants.length === 0 ? (
+
No restaurants found.
+ ) : (
+
+
+
+
+ Name
+ Email
+ Phone
+ Status
+ Actions
+
+
+
+ {restaurants.map(restaurant => (
+
+ {restaurant.name}
+ {restaurant.email}
+ {restaurant.telephoneNumber}
+
+
+ {restaurant.isVerified ? 'Verified' : 'Pending'}
+
+
+
+ {!restaurant.isVerified && (
+ handleVerifyRestaurant(restaurant._id)}
+ className={styles.verifyButton}
+ >
+ Verify
+
+ )}
+ handleDeleteRestaurant(restaurant._id)}
+ className={styles.deleteButton}
+ >
+ Delete
+
+
+
+ ))}
+
+
+
+ )}
+
+
+ );
+};
+
+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}
+
window.location.reload()}>Retry
+
+
+ );
+ }
+
+ return (
+
+ {/* Cover Photo Banner */}
+
+ {restaurantData?.coverImage ? (
+
{
+ 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
+
+
+ {
+ localStorage.removeItem('token');
+ localStorage.removeItem('restaurant');
+ navigate('/login');
+ }}
+ >
+ Logout
+
+
+
+
+ {/* Profile Section */}
+
+ {restaurantData?.logo ? (
+
{
+ 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.'}
+
+
+
+ {restaurantData?.isOpen ? 'Set as Closed' : 'Set as Open'}
+
+
+
+
+ {/* 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 ? (
+
+
{
+ 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 (
+
+ );
+};
+
+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"}
+
+
+
+
+
+
Menu Items ({menuItems.length})
+ {menuItems.length > 0 && (
+
+ Delete All
+
+ )}
+
+
+ {error &&
{error}
}
+
+ {menuItems.length === 0 ? (
+
+
No menu items found. Add your first menu item using the form.
+
+ ) : (
+
+ {menuItems.map((item) => (
+
+ {item.image && (
+
+
{
+ console.error("Image failed to load:", item.image);
+ e.target.src = "https://via.placeholder.com/100?text=No+Image";
+ e.target.style.background = 'var(--input-background)';
+ e.target.style.color = 'var(--text-color)';
+ e.target.style.padding = '10px';
+ e.target.style.textAlign = 'center';
+ e.target.style.fontSize = '12px';
+ }}
+ />
+
+ )}
+
+
{item.name}
+
{item.description || "No description"}
+
Rs. {parseFloat(item.price).toFixed(2)}
+
+
+ handleEdit(item)}
+ className={styles.editButton}
+ >
+ Edit
+
+ handleDelete(item._id)}
+ className={styles.deleteButton}
+ >
+ Delete
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+};
+
+export default MenuManagement;
\ No newline at end of file
diff --git a/frontend/src/Pages/Restaurant/MenuManagement.module.css b/frontend/src/Pages/Restaurant/MenuManagement.module.css
new file mode 100644
index 0000000..c6aa03b
--- /dev/null
+++ b/frontend/src/Pages/Restaurant/MenuManagement.module.css
@@ -0,0 +1,358 @@
+/* src/pages/MenuManagement.module.css */
+@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); }
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+ color: var(--text-color);
+ background-color: var(--background-color);
+ transition: all 0.3s ease;
+}
+
+.title {
+ color: var(--text-color);
+ margin-bottom: 2rem;
+ font-size: 2rem;
+ text-align: center;
+ transition: color 0.3s ease;
+}
+
+.content {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ gap: 2rem;
+}
+
+@media (max-width: 768px) {
+ .content {
+ grid-template-columns: 1fr;
+ }
+}
+
+.formSection {
+ background: var(--card-background);
+ border-radius: 10px;
+ padding: 1.5rem;
+ box-shadow: 0 4px 12px var(--shadow-color);
+ transition: background-color 0.3s ease, box-shadow 0.3s ease;
+}
+
+.formSection h2 {
+ margin-bottom: 1.5rem;
+ color: var(--text-color);
+ font-size: 1.25rem;
+ transition: color 0.3s ease;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.formGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.formGroup label {
+ font-weight: 600;
+ color: var(--text-color);
+ transition: color 0.3s ease;
+}
+
+.input {
+ padding: 0.8rem;
+ border: 1px solid var(--border-color);
+ border-radius: 5px;
+ font-size: 1rem;
+ transition: all 0.3s ease;
+ background: var(--input-background);
+ color: var(--text-color);
+}
+
+.textarea {
+ composes: input;
+ min-height: 100px;
+ resize: vertical;
+}
+
+.input:focus, .textarea:focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(var(--primary-color-rgb), 0.2);
+ outline: none;
+}
+
+.fileInput {
+ display: none;
+}
+
+.fileLabel {
+ display: block;
+ padding: 0.8rem;
+ background: var(--input-background);
+ border: 2px dashed var(--border-color);
+ border-radius: 5px;
+ text-align: center;
+ cursor: pointer;
+ color: var(--text-secondary);
+ transition: all 0.3s ease;
+}
+
+.fileLabel:hover {
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+.button {
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
+ color: var(--button-text);
+ border: none;
+ padding: 0.8rem 1rem;
+ border-radius: 5px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.button:hover {
+ box-shadow: 0 5px 15px var(--shadow-color);
+ transform: translateY(-2px);
+}
+
+.cancelButton {
+ background: var(--hover-background);
+ color: var(--text-color);
+ border: 1px solid var(--border-color);
+ padding: 0.8rem 1rem;
+ border-radius: 5px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.cancelButton:hover {
+ background: var(--input-background);
+}
+
+.buttonGroup {
+ display: flex;
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+.menuSection {
+ background: var(--card-background);
+ border-radius: 10px;
+ padding: 1.5rem;
+ box-shadow: 0 4px 12px var(--shadow-color);
+ transition: background-color 0.3s ease, box-shadow 0.3s ease;
+}
+
+.menuHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+}
+
+.menuHeader h2 {
+ color: var(--text-color);
+ font-size: 1.25rem;
+ transition: color 0.3s ease;
+}
+
+.dangerButton {
+ background: var(--error-color);
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 5px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.dangerButton:hover {
+ filter: brightness(90%);
+ transform: translateY(-2px);
+}
+
+.menuItemsList {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ max-height: 600px;
+ overflow-y: auto;
+ padding-right: 0.5rem;
+}
+
+.menuItemsList::-webkit-scrollbar {
+ width: 8px;
+}
+
+.menuItemsList::-webkit-scrollbar-track {
+ background: var(--input-background);
+ border-radius: 4px;
+}
+
+.menuItemsList::-webkit-scrollbar-thumb {
+ background-color: var(--border-color);
+ border-radius: 4px;
+}
+
+.menuItemCard {
+ display: flex;
+ background: var(--hover-background);
+ border-radius: 8px;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ border: 1px solid var(--border-color);
+}
+
+.menuItemCard:hover {
+ box-shadow: 0 4px 8px var(--shadow-color);
+ transform: translateY(-2px);
+}
+
+.imageContainer {
+ width: 100px;
+ height: 100px;
+ flex-shrink: 0;
+ background: var(--input-background);
+}
+
+.itemImage {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.itemContent {
+ padding: 1rem;
+ flex: 1;
+}
+
+.itemContent h3 {
+ margin: 0 0 0.5rem;
+ color: var(--text-color);
+ transition: color 0.3s ease;
+}
+
+.description {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ margin-bottom: 0.5rem;
+ transition: color 0.3s ease;
+}
+
+.price {
+ font-weight: 700;
+ color: var(--primary-color);
+ transition: color 0.3s ease;
+}
+
+.itemActions {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.5rem;
+ background: var(--input-background);
+ transition: background-color 0.3s ease;
+}
+
+.editButton, .deleteButton {
+ padding: 0.5rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: all 0.2s ease;
+}
+
+.editButton {
+ background: var(--primary-color);
+ color: var(--button-text);
+}
+
+.editButton:hover {
+ background: var(--secondary-color);
+ transform: translateY(-2px);
+}
+
+.deleteButton {
+ background: var(--error-color);
+ color: white;
+}
+
+.deleteButton:hover {
+ filter: brightness(90%);
+ transform: translateY(-2px);
+}
+
+.emptyState {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 200px;
+ color: var(--text-secondary);
+ transition: color 0.3s ease;
+}
+
+.error, .success {
+ padding: 1rem;
+ border-radius: 5px;
+ margin-bottom: 1rem;
+ font-weight: 500;
+ text-align: center;
+ animation: fadeIn 0.3s ease;
+}
+
+.error {
+ background: rgba(231, 76, 60, 0.1);
+ border: 1px solid rgba(231, 76, 60, 0.3);
+ color: var(--error-color);
+}
+
+.success {
+ background: rgba(39, 174, 96, 0.1);
+ border: 1px solid rgba(39, 174, 96, 0.3);
+ color: var(--success-color);
+}
+
+.loadingContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 400px;
+ color: var(--text-color);
+ transition: color 0.3s ease;
+}
+
+.loader {
+ width: 40px;
+ height: 40px;
+ border: 4px solid var(--border-color);
+ border-top: 4px solid var(--primary-color);
+ border-radius: 50%;
+ animation: rotate 1s linear infinite;
+ margin-bottom: 1rem;
+}
\ No newline at end of file
diff --git a/frontend/src/Pages/Restaurant/ProfileSettings.jsx b/frontend/src/Pages/Restaurant/ProfileSettings.jsx
new file mode 100644
index 0000000..90b8e81
--- /dev/null
+++ b/frontend/src/Pages/Restaurant/ProfileSettings.jsx
@@ -0,0 +1,634 @@
+// src/pages/ProfileSettings.jsx
+import React, { useState, useEffect } from "react";
+import { useNavigate, useLocation } from "react-router-dom"; // Import useLocation
+import axios from "../services/axios";
+import styles from "./ProfileSettings.module.css";
+
+const ProfileSettings = () => {
+ const [restaurant, setRestaurant] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [message, setMessage] = useState("");
+ const [activeTab, setActiveTab] = useState("profile"); // Default tab
+ const [confirmDelete, setConfirmDelete] = useState(false);
+ const [formData, setFormData] = useState({
+ name: "",
+ description: "",
+ telephoneNumber: "",
+ location: {
+ address: "",
+ city: "",
+ postalCode: "",
+ coordinates: {
+ latitude: 0,
+ longitude: 0
+ }
+ },
+ cuisine: "",
+ logo: null,
+ coverImage: null,
+ });
+
+ const navigate = useNavigate();
+ const location = useLocation(); // Get location object
+
+ useEffect(() => {
+ // Check if user is logged in
+ const token = localStorage.getItem("token");
+ if (!token) {
+ navigate("/login");
+ return;
+ }
+
+ // Set initial tab based on navigation state, only on initial mount if desired
+ if (location.state?.initialTab) {
+ setActiveTab(location.state.initialTab);
+ // Optional: Clear the state after using it to prevent re-triggering on refresh/re-render
+ // navigate(location.pathname, { replace: true, state: {} });
+ }
+
+ // Fetch restaurant data
+ const fetchRestaurant = async () => {
+ try {
+ setLoading(true);
+ // Get restaurant ID from localStorage
+ const storedRestaurant = JSON.parse(localStorage.getItem("restaurant"));
+ if (!storedRestaurant || !storedRestaurant.id) {
+ throw new Error("Restaurant information not found");
+ }
+
+ // Use getRestaurantById instead of /me endpoint
+ const response = await axios.get(`/api/restaurants/${storedRestaurant.id}`, {
+ headers: {
+ Authorization: `Bearer ${token}`
+ }
+ });
+
+ setRestaurant(response.data);
+
+ // Populate form with restaurant data
+ setFormData({
+ name: response.data.name || "",
+ description: response.data.description || "",
+ telephoneNumber: response.data.telephoneNumber || "",
+ location: {
+ address: response.data.location?.address || "",
+ city: response.data.location?.city || "",
+ postalCode: response.data.location?.postalCode || "",
+ coordinates: {
+ latitude: response.data.location?.coordinates?.latitude || 0,
+ longitude: response.data.location?.coordinates?.longitude || 0
+ }
+ },
+ cuisine: response.data.cuisine?.join(", ") || "",
+ logo: null,
+ coverImage: null,
+ });
+ } catch (err) {
+ console.error("Error fetching restaurant data:", err);
+ setError("Failed to load restaurant data. Please try logging in again.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchRestaurant();
+ // Add location.state?.initialTab to dependency array if you want it to react to state changes
+ }, [navigate]); // Keep dependencies minimal if you only want this on mount
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ if (name.startsWith("location.")) {
+ const locationField = name.split(".")[1];
+ setFormData({
+ ...formData,
+ location: {
+ ...formData.location,
+ [locationField]: value
+ }
+ });
+ } else {
+ setFormData({ ...formData, [name]: value });
+ }
+ };
+
+ const handleFileChange = (e) => {
+ const { name, files } = e.target;
+ if (files.length > 0) {
+ const file = files[0];
+ const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
+
+ if (!validTypes.includes(file.type)) {
+ setMessage(`Error: Only JPEG, JPG, PNG, and GIF images are allowed.`);
+ e.target.value = ''; // Reset the file input
+ return;
+ }
+
+ setFormData({ ...formData, [name]: files[0] });
+ } else {
+ setFormData({ ...formData, [name]: null });
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setMessage("");
+
+ try {
+ const storedRestaurant = JSON.parse(localStorage.getItem("restaurant"));
+ if (!storedRestaurant || !storedRestaurant.id) {
+ throw new Error("Restaurant information not found");
+ }
+
+ const restaurantId = storedRestaurant.id;
+
+ // Create FormData to send all data including files
+ const profileFormData = new FormData();
+
+ // Append text fields
+ profileFormData.append("name", formData.name);
+ profileFormData.append("description", formData.description);
+ profileFormData.append("telephoneNumber", formData.telephoneNumber);
+ // Stringify location object before appending
+ profileFormData.append("location", JSON.stringify(formData.location));
+ profileFormData.append("cuisine", formData.cuisine); // Send as comma-separated string
+
+ // Append files only if they exist in the state
+ if (formData.logo instanceof File) {
+ profileFormData.append("logo", formData.logo);
+ }
+ if (formData.coverImage instanceof File) {
+ profileFormData.append("coverImage", formData.coverImage);
+ }
+
+ // Update restaurant profile using PUT with FormData
+ const response = await axios.put(`/api/restaurants/${restaurantId}`, profileFormData, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
+ // Content-Type is set automatically by browser for FormData
+ }
+ });
+
+ setMessage("Profile updated successfully!");
+
+ // Update local state and storage with potentially new image paths from response
+ setRestaurant(response.data.restaurant); // Update local restaurant state
+ if (storedRestaurant) {
+ storedRestaurant.name = response.data.restaurant.name;
+ storedRestaurant.logo = response.data.restaurant.logo; // Update logo path
+ storedRestaurant.coverImage = response.data.restaurant.coverImage; // Update cover image path
+ localStorage.setItem("restaurant", JSON.stringify(storedRestaurant));
+ }
+
+ // Reset file inputs in state after successful upload
+ setFormData(prev => ({
+ ...prev,
+ logo: null,
+ coverImage: null
+ }));
+ // Optionally clear file input elements visually if needed (though state reset is key)
+ // document.getElementById('logo').value = null;
+ // document.getElementById('coverImage').value = null;
+
+ // No need to manually refresh, response data updates state
+
+ } catch (err) {
+ console.error("Error updating profile:", err);
+ setMessage("Failed to update profile. " + (err.response?.data?.message || "Please try again."));
+ }
+ };
+
+ const handleDeleteAccount = async () => {
+ try {
+ const storedRestaurant = JSON.parse(localStorage.getItem("restaurant"));
+ if (!storedRestaurant || !storedRestaurant.id) {
+ throw new Error("Restaurant information not found");
+ }
+
+ await axios.delete(`/api/restaurants/${storedRestaurant.id}`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem("token")}`
+ }
+ });
+
+ // Clear localStorage and redirect to login
+ localStorage.removeItem("token");
+ localStorage.removeItem("restaurant");
+ navigate("/login", { state: { message: "Your restaurant account has been deleted successfully." } });
+
+ } catch (err) {
+ console.error("Error deleting account:", err);
+ setMessage("Failed to delete account. " + (err.response?.data?.message || "Please try again."));
+ setConfirmDelete(false);
+ }
+ };
+
+ const handleToggleAvailability = async () => {
+ try {
+ const newStatus = !restaurant.isOpen;
+
+ await axios.post("/api/restaurants/availability",
+ {
+ restaurantId: restaurant._id,
+ isOpen: newStatus
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem("token")}`
+ }
+ }
+ );
+
+ // Update local state
+ setRestaurant({
+ ...restaurant,
+ isOpen: newStatus
+ });
+
+ setMessage(`Restaurant is now ${newStatus ? "Open" : "Closed"} for orders.`);
+
+ } catch (err) {
+ console.error("Error toggling availability:", err);
+ setMessage("Failed to update restaurant availability. Please try again.");
+ }
+ };
+
+ // Show loading state
+ if (loading) {
+ return (
+
+
+
Loading profile settings...
+
+ );
+ }
+
+ // Show error state
+ if (error) {
+ return (
+
+
Something went wrong
+
{error}
+
navigate("/dashboard")}>
+ Back to Dashboard
+
+
+ );
+ }
+
+ return (
+
+
+
+ {message && (
+
+
+ {message.includes("success") || message.includes("Open") || message.includes("Closed") ? "✓" : "!"}
+
+ {message}
+ setMessage("")}>×
+
+ )}
+
+
+
+
+ {restaurant?.logo ? (
+
+ ) : (
+
+ {restaurant?.name?.charAt(0) || "R"}
+
+ )}
+
{restaurant?.name}
+
+ {restaurant?.isOpen ? "Open" : "Closed"}
+
+
+
+
+ setActiveTab('profile')}
+ >
+ 👤 Profile Information
+
+ setActiveTab('appearance')}
+ >
+ 🖼️ Restaurant Appearance
+
+ setActiveTab('location')}
+ >
+ 📍 Location Details
+
+ setActiveTab('account')}
+ >
+ ⚙️ Account Settings
+
+
+
+
+ Restaurant Status:
+
+ {restaurant?.isOpen ? "Set as Closed" : "Set as Open"}
+
+
+
+
+
+ {/* Conditional rendering based on activeTab */}
+ {activeTab === 'profile' && (
+
+ )}
+
+ {/* Appearance Tab */}
+ {activeTab === 'appearance' && (
+
+ Restaurant Appearance
+ {/* Use the main handleSubmit for this section's button too */}
+
+
+
+
Cover Image
+
+ {restaurant?.coverImage ? (
+
+ ) : (
+
+ No Cover Image
+
+ )}
+
+
+
+
+ {formData.coverImage ? formData.coverImage.name : "Choose New Cover Image"}
+
+
+
+
+
+
Restaurant Logo
+
+ {restaurant?.logo ? (
+
+ ) : (
+
+ {restaurant?.name?.charAt(0) || "R"}
+
+ )}
+
+
+
+
+ {/* Show file name if selected, otherwise prompt */}
+ {formData.logo instanceof File ? formData.logo.name : "Choose New Logo"}
+
+
+
+
+
+
+
Recommended dimensions:
+
+ Cover Image: 1200×300 pixels
+ Logo: 500×500 pixels
+
+
+
+ {/* Button triggers the form's onSubmit */}
+
+ Save Appearance Settings
+
+
+
+ )}
+
+ {activeTab === 'location' && (
+
+ Location Details
+
+
+ Street Address
+
+
+
+
+
+
+
Map feature coming soon. Your restaurant will be displayed at the provided address.
+
+
+
+ Save Location Details
+
+
+
+ )}
+
+ {activeTab === 'account' && (
+
+ Account Settings
+
+
+
+
Account Status
+
+ {restaurant?.isVerified ? "Verified Account" : "Pending Verification"}
+
+
+
+
+
Restaurant ID
+ {restaurant?._id}
+
+
+
+
+
Danger Zone
+
+ Permanently delete your restaurant account and all associated data. This action cannot be undone.
+
+
+ {!confirmDelete ? (
+
setConfirmDelete(true)}
+ className={styles.deleteButton}
+ >
+ Delete Account
+
+ ) : (
+
+
+
⚠️
+
Are you absolutely sure you want to delete your account? All your data will be permanently removed.
+
+
+
+ Yes, Delete My Account
+
+ setConfirmDelete(false)}
+ className={styles.cancelButton}
+ >
+ Cancel
+
+
+
+ )}
+
+
+ )}
+
+
+
+ );
+};
+
+export default ProfileSettings;
\ No newline at end of file
diff --git a/frontend/src/Pages/Restaurant/ProfileSettings.module.css b/frontend/src/Pages/Restaurant/ProfileSettings.module.css
new file mode 100644
index 0000000..4ad3c6f
--- /dev/null
+++ b/frontend/src/Pages/Restaurant/ProfileSettings.module.css
@@ -0,0 +1,704 @@
+/* src/pages/ProfileSettings.module.css */
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes slideUp {
+ from { transform: translateY(20px); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+}
+
+@keyframes rotate {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.container {
+ background-color: var(--background-color);
+ min-height: 100vh;
+ color: var(--text-color);
+}
+
+.header {
+ background: var(--card-background);
+ border-bottom: 1px solid var(--border-color);
+ padding: 1rem 2rem;
+ box-shadow: 0 2px 10px var(--shadow-color);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.headerContent {
+ max-width: 1400px;
+ margin: 0 auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.header h1 {
+ color: var(--text-color);
+ font-size: 1.5rem;
+ font-weight: 700;
+}
+
+.backButton {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: var(--text-color);
+ background: var(--hover-background);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 0.5rem 1rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-size: 0.9rem;
+}
+
+.backIcon {
+ font-size: 1.2rem;
+}
+
+.backButton:hover {
+ background: var(--input-background);
+ transform: translateY(-1px);
+}
+
+.notification {
+ position: fixed;
+ top: 80px;
+ right: 20px;
+ padding: 1rem;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ animation: slideUp 0.3s ease;
+ z-index: 1000;
+ box-shadow: 0 4px 12px var(--shadow-color);
+ max-width: 400px;
+}
+
+.success {
+ background-color: rgba(46, 204, 113, 0.1);
+ border: 1px solid rgba(46, 204, 113, 0.3);
+ color: var(--success-color);
+}
+
+.error {
+ background-color: rgba(231, 76, 60, 0.1);
+ border: 1px solid rgba(231, 76, 60, 0.3);
+ color: var(--error-color);
+}
+
+.notificationIcon {
+ font-size: 1.2rem;
+ font-weight: bold;
+}
+
+.closeNotification {
+ margin-left: auto;
+ background: none;
+ border: none;
+ font-size: 1.2rem;
+ cursor: pointer;
+ color: inherit;
+ opacity: 0.7;
+}
+
+.closeNotification:hover {
+ opacity: 1;
+}
+
+.content {
+ display: flex;
+ max-width: 1400px;
+ margin: 2rem auto;
+ padding: 0 2rem;
+ gap: 2rem;
+}
+
+.sidebar {
+ width: 280px;
+ background: var(--card-background);
+ border-radius: 12px;
+ padding: 1.5rem;
+ position: sticky;
+ top: 100px;
+ height: fit-content;
+ box-shadow: 0 4px 12px var(--shadow-color);
+}
+
+.profileSummary {
+ text-align: center;
+ padding-bottom: 1.5rem;
+ margin-bottom: 1.5rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.profileAvatar {
+ width: 100px;
+ height: 100px;
+ border-radius: 50%;
+ object-fit: cover;
+ margin: 0 auto 1rem;
+ border: 3px solid var(--border-color);
+ box-shadow: 0 4px 8px var(--shadow-color);
+}
+
+.profileAvatarPlaceholder {
+ width: 100px;
+ height: 100px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 2rem;
+ font-weight: bold;
+ color: var(--button-text);
+ border-radius: 50%;
+ margin: 0 auto 1rem;
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
+}
+
+.profileName {
+ font-size: 1.2rem;
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+ color: var(--text-color);
+}
+
+.profileStatus {
+ display: inline-block;
+ padding: 0.3rem 0.8rem;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 600;
+}
+
+.statusOpen {
+ background-color: rgba(46, 204, 113, 0.1);
+ color: var(--success-color);
+}
+
+.statusClosed {
+ background-color: rgba(231, 76, 60, 0.1);
+ color: var(--error-color);
+}
+
+.settingsNav {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ margin-bottom: 1.5rem;
+}
+
+.navButton {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ text-align: left;
+ padding: 0.8rem 1rem;
+ border-radius: 8px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.navButton:hover {
+ background: var(--hover-background);
+ color: var(--primary-color);
+}
+
+.navButton.active {
+ background: var(--hover-background);
+ color: var(--primary-color);
+ font-weight: 600;
+}
+
+.navIcon {
+ font-size: 1.2rem;
+}
+
+.statusToggleContainer {
+ padding-top: 1.5rem;
+ border-top: 1px solid var(--border-color);
+}
+
+.statusToggleLabel {
+ display: block;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ margin-bottom: 0.75rem;
+}
+
+.statusToggleButton {
+ width: 100%;
+ padding: 0.8rem;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-weight: 600;
+ border: none;
+}
+
+.openButton {
+ background-color: rgba(231, 76, 60, 0.1);
+ color: var(--error-color);
+}
+
+.openButton:hover {
+ background-color: rgba(231, 76, 60, 0.2);
+}
+
+.closedButton {
+ background-color: rgba(46, 204, 113, 0.1);
+ color: var(--success-color);
+}
+
+.closedButton:hover {
+ background-color: rgba(46, 204, 113, 0.2);
+}
+
+.mainContent {
+ flex: 1;
+}
+
+.settingsSection {
+ background: var(--card-background);
+ border-radius: 12px;
+ padding: 2rem;
+ box-shadow: 0 4px 12px var(--shadow-color);
+ animation: fadeIn 0.5s ease;
+}
+
+.sectionTitle {
+ font-size: 1.5rem;
+ font-weight: 700;
+ margin-bottom: 2rem;
+ color: var(--text-color);
+ padding-bottom: 1rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.formGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.formRow {
+ display: flex;
+ gap: 1.5rem;
+}
+
+.formRow .formGroup {
+ flex: 1;
+}
+
+.label {
+ font-weight: 600;
+ color: var(--text-color);
+ font-size: 0.95rem;
+}
+
+.input, .textarea, .select {
+ 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, .textarea:hover, .select:hover {
+ border-color: var(--text-secondary);
+}
+
+.input:focus, .textarea:focus, .select:focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(230, 126, 34, 0.2);
+ background: var(--card-background);
+}
+
+.textarea {
+ resize: vertical;
+ min-height: 120px;
+}
+
+.inputHint {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ margin-top: 0.3rem;
+}
+
+.saveButton {
+ margin-top: 1rem;
+ 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: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.saveButton:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 5px 15px var(--shadow-color);
+}
+
+.saveButton:active {
+ transform: translateY(0);
+}
+
+.imagesPreview {
+ display: flex;
+ gap: 2rem;
+ margin-bottom: 2rem;
+}
+
+.previewSection {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.previewTitle {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--text-color);
+}
+
+.coverImageContainer {
+ width: 100%;
+ height: 200px;
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid var(--border-color);
+}
+
+.coverImage {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.coverPlaceholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 1rem;
+ color: var(--text-secondary);
+ background: linear-gradient(135deg, var(--hover-background) 0%, var(--primary-color) 20%);
+ opacity: 0.3;
+}
+
+.logoImageContainer {
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ overflow: hidden;
+ margin: 0 auto;
+ border: 1px solid var(--border-color);
+}
+
+.logoImage {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.logoPlaceholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 2rem;
+ font-weight: bold;
+ color: var(--button-text);
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
+}
+
+.imageUploadContainer {
+ margin-top: 0.5rem;
+}
+
+.fileInput {
+ display: none;
+}
+
+.fileLabel {
+ display: block;
+ padding: 0.8rem 1rem;
+ text-align: center;
+ border: 2px dashed var(--border-color);
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.fileLabel:hover {
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+.appearanceHint {
+ background: var(--hover-background);
+ border-radius: 8px;
+ padding: 1rem;
+ margin-bottom: 1.5rem;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+}
+
+.appearanceHint p {
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+}
+
+.appearanceHint ul {
+ margin-left: 1.5rem;
+}
+
+.mapPlaceholder {
+ background: var(--hover-background);
+ border-radius: 8px;
+ height: 200px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ color: var(--text-secondary);
+ margin: 1rem 0;
+ border: 1px dashed var(--border-color);
+}
+
+.accountInfo {
+ margin-bottom: 2rem;
+}
+
+.infoItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 0;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.infoTitle {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--text-color);
+}
+
+.infoValue {
+ font-size: 0.95rem;
+}
+
+.verified {
+ color: var(--success-color);
+ font-weight: 600;
+}
+
+.pending {
+ color: var(--primary-color);
+ font-weight: 600;
+}
+
+.dangerZone {
+ background: rgba(231, 76, 60, 0.05);
+ border: 1px solid rgba(231, 76, 60, 0.2);
+ border-radius: 8px;
+ padding: 1.5rem;
+ margin-top: 2rem;
+}
+
+.dangerTitle {
+ color: var(--error-color);
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+}
+
+.dangerDescription {
+ color: var(--text-secondary);
+ margin-bottom: 1.5rem;
+ line-height: 1.5;
+}
+
+.deleteButton {
+ padding: 0.8rem 1.5rem;
+ background: var(--error-color);
+ color: var(--button-text);
+ border: none;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.deleteButton:hover {
+ box-shadow: 0 4px 12px rgba(231, 76, 60, 0.2);
+ transform: translateY(-1px);
+}
+
+.confirmDelete {
+ animation: fadeIn 0.3s ease;
+}
+
+.confirmMessage {
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
+ margin-bottom: 1.5rem;
+ padding: 1rem;
+ background: rgba(231, 76, 60, 0.1);
+ border-radius: 8px;
+}
+
+.warningIcon {
+ font-size: 1.5rem;
+}
+
+.confirmButtons {
+ display: flex;
+ gap: 1rem;
+}
+
+.confirmDeleteButton {
+ padding: 0.8rem 1.5rem;
+ background: var(--error-color);
+ color: var(--button-text);
+ border: none;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.confirmDeleteButton:hover {
+ box-shadow: 0 4px 12px rgba(231, 76, 60, 0.2);
+}
+
+.cancelButton {
+ padding: 0.8rem 1.5rem;
+ background: var(--hover-background);
+ color: var(--text-color);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.cancelButton:hover {
+ background: var(--input-background);
+}
+
+.loadingContainer {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ background: var(--background-color);
+ color: var(--text-color);
+}
+
+.loader {
+ width: 50px;
+ height: 50px;
+ border: 5px solid var(--border-color);
+ border-top-color: var(--primary-color);
+ border-radius: 50%;
+ animation: rotate 1s linear infinite;
+ margin-bottom: 1.5rem;
+}
+
+.errorContainer {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding: 2rem;
+ background: var(--background-color);
+ color: var(--text-color);
+}
+
+/* Responsive styles */
+@media (max-width: 1200px) {
+ .content {
+ padding: 0 1.5rem;
+ }
+}
+
+@media (max-width: 992px) {
+ .content {
+ flex-direction: column;
+ }
+
+ .sidebar {
+ width: 100%;
+ position: static;
+ margin-bottom: 2rem;
+ }
+
+ .settingsNav {
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+
+ .navButton {
+ flex: 1;
+ min-width: 150px;
+ }
+}
+
+@media (max-width: 768px) {
+ .imagesPreview {
+ flex-direction: column;
+ }
+
+ .formRow {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .header {
+ padding: 1rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .headerContent {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-start;
+ }
+
+ .confirmButtons {
+ flex-direction: column;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/Pages/Restaurant/Register.jsx b/frontend/src/Pages/Restaurant/Register.jsx
new file mode 100644
index 0000000..d1d9a78
--- /dev/null
+++ b/frontend/src/Pages/Restaurant/Register.jsx
@@ -0,0 +1,752 @@
+import React, { useState, useEffect, useRef } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import axios from "../services/axios";
+import styles from './Register.module.css';
+
+const Register = ({ darkMode }) => {
+ const navigate = useNavigate();
+ const firstInputRef = useRef(null);
+
+ // Form data state
+ const [formData, setFormData] = useState({
+ name: "",
+ email: "",
+ password: "",
+ confirmPassword: "", // Added confirm password field
+ description: "",
+ telephoneNumber: "",
+ location: {
+ address: "",
+ city: "",
+ postalCode: "",
+ coordinates: {
+ latitude: 0,
+ longitude: 0
+ }
+ },
+ cuisine: "",
+ logo: null,
+ coverImage: null,
+ });
+
+ // Form state management
+ const [errors, setErrors] = useState({});
+ const [touched, setTouched] = useState({});
+ const [message, setMessage] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [passwordStrength, setPasswordStrength] = useState(0);
+ const [showPassword, setShowPassword] = useState(false);
+
+ // Focus first input on component mount
+ useEffect(() => {
+ if (firstInputRef.current) {
+ firstInputRef.current.focus();
+ }
+ }, []);
+
+ // Password strength evaluation
+ useEffect(() => {
+ if (!formData.password) {
+ setPasswordStrength(0);
+ return;
+ }
+
+ let strength = 0;
+ // Length check
+ if (formData.password.length >= 8) strength += 1;
+ // Contains uppercase
+ if (/[A-Z]/.test(formData.password)) strength += 1;
+ // Contains lowercase
+ if (/[a-z]/.test(formData.password)) strength += 1;
+ // Contains number
+ if (/[0-9]/.test(formData.password)) strength += 1;
+ // Contains special char
+ if (/[!@#$%^&*(),.?":{}|<>]/.test(formData.password)) strength += 1;
+
+ setPasswordStrength(strength);
+ }, [formData.password]);
+
+ const validateField = (name, value) => {
+ let error = "";
+
+ switch(name) {
+ case "name":
+ if (!value.trim()) {
+ error = "Restaurant name is required";
+ } else if (value.length < 2) {
+ error = "Restaurant name must be at least 2 characters";
+ } else if (value.length > 50) {
+ error = "Restaurant name cannot exceed 50 characters";
+ }
+ break;
+
+ 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";
+ } else if (value.length < 8) {
+ error = "Password must be at least 8 characters";
+ } else if (!/[A-Z]/.test(value)) {
+ error = "Password must contain at least one uppercase letter";
+ } else if (!/[a-z]/.test(value)) {
+ error = "Password must contain at least one lowercase letter";
+ } else if (!/[0-9]/.test(value)) {
+ error = "Password must contain at least one number";
+ } else if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
+ error = "Password must contain at least one special character";
+ }
+ break;
+
+ case "confirmPassword":
+ if (!value) {
+ error = "Please confirm your password";
+ } else if (value !== formData.password) {
+ error = "Passwords do not match";
+ }
+ break;
+
+ case "description":
+ if (!value.trim()) {
+ error = "Description is required";
+ } else if (value.length < 20) {
+ error = "Description must be at least 20 characters";
+ } else if (value.length > 500) {
+ error = "Description cannot exceed 500 characters";
+ }
+ break;
+
+ case "telephoneNumber":
+ const phoneRegex = /^\+?[0-9]{10,15}$/;
+ if (!value.trim()) {
+ error = "Telephone number is required";
+ } else if (!phoneRegex.test(value.replace(/[\s-]/g, ''))) {
+ error = "Please enter a valid telephone number (10-15 digits)";
+ }
+ break;
+
+ case "location.address":
+ if (!value.trim()) {
+ error = "Address is required";
+ } else if (value.length > 100) {
+ error = "Address cannot exceed 100 characters";
+ }
+ break;
+
+ case "location.city":
+ if (!value.trim()) {
+ error = "City is required";
+ } else if (value.length > 50) {
+ error = "City name cannot exceed 50 characters";
+ }
+ break;
+
+ case "location.postalCode":
+ const postalRegex = /^[0-9a-zA-Z\s-]{3,10}$/;
+ if (!value.trim()) {
+ error = "Postal code is required";
+ } else if (!postalRegex.test(value)) {
+ error = "Please enter a valid postal code (3-10 alphanumeric characters)";
+ }
+ break;
+
+ case "cuisine":
+ if (value) {
+ if (value.length > 100) {
+ error = "Cuisine list cannot exceed 100 characters";
+ } else if (!/^[a-zA-Z\s,]+$/.test(value)) {
+ error = "Cuisine should only contain letters, spaces, and commas";
+ }
+ }
+ break;
+
+ case "logo":
+ if (!value) {
+ error = "Restaurant logo is required";
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return error;
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+
+ // Validate all fields
+ for (const field in formData) {
+ if (field === 'location') {
+ for (const locationField in formData.location) {
+ if (locationField !== 'coordinates') {
+ const error = validateField(`location.${locationField}`, formData.location[locationField]);
+ if (error) newErrors[`location.${locationField}`] = error;
+ }
+ }
+ } else if (field !== 'coverImage') { // Cover image is optional
+ const value = formData[field];
+ const error = validateField(field, value);
+ if (error) newErrors[field] = error;
+ }
+ }
+
+ 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;
+
+ if (touched[name]) {
+ const error = validateField(name, value);
+ setErrors(prev => ({ ...prev, [name]: error }));
+ }
+
+ if (name.startsWith("location.")) {
+ const locationField = name.split(".")[1];
+ setFormData({
+ ...formData,
+ location: {
+ ...formData.location,
+ [locationField]: value
+ }
+ });
+ } else {
+ setFormData({ ...formData, [name]: value });
+ }
+ };
+
+ const handleFileChange = (e) => {
+ const { name, files } = e.target;
+ setTouched(prev => ({ ...prev, [name]: true }));
+
+ if (files.length > 0) {
+ const file = files[0];
+ const fileSize = file.size / 1024 / 1024; // in MB
+ const validTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/gif'];
+
+ let error = "";
+
+ if (!validTypes.includes(file.type)) {
+ error = "Only image files (JPEG, PNG, GIF) are allowed";
+ } else if (fileSize > 5) {
+ error = "File size cannot exceed 5MB";
+ } else if (file.name.length > 100) {
+ error = "File name is too long (max 100 characters)";
+ }
+
+ setErrors(prev => ({ ...prev, [name]: error }));
+
+ if (!error) {
+ setFormData({ ...formData, [name]: file });
+ }
+ } else {
+ // If user cancels file selection and field is required
+ if (name === 'logo' && !formData.logo) {
+ setErrors(prev => ({ ...prev, [name]: "Restaurant logo is required" }));
+ }
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setMessage("");
+
+ // Mark all fields as touched
+ const allTouched = Object.keys(formData).reduce((acc, field) => {
+ if (field === 'location') {
+ return {
+ ...acc,
+ 'location.address': true,
+ 'location.city': true,
+ 'location.postalCode': true
+ };
+ }
+ return { ...acc, [field]: true };
+ }, {});
+
+ setTouched(allTouched);
+
+ // Validate form before submission
+ if (!validateForm()) {
+ // Find the first error field and scroll to it
+ const firstErrorField = Object.keys(errors)[0];
+ const errorElement = document.querySelector(`[name="${firstErrorField}"]`);
+
+ if (errorElement) {
+ errorElement.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center'
+ });
+ errorElement.focus();
+ }
+ return;
+ }
+
+ // Remove confirmPassword from data to be sent
+ const { confirmPassword, ...dataToSubmit } = formData;
+
+ setIsLoading(true);
+ const form = new FormData();
+
+ Object.keys(dataToSubmit).forEach((key) => {
+ if (key === 'location') {
+ form.append(key, JSON.stringify(dataToSubmit[key]));
+ } else {
+ form.append(key, dataToSubmit[key]);
+ }
+ });
+
+ try {
+ const response = await axios.post("/api/auth/register", form);
+ setMessage(response.data.message || "Registration successful!");
+
+ // Clear form after successful submission
+ if (response.data.token) {
+ setFormData({
+ name: "",
+ email: "",
+ password: "",
+ confirmPassword: "",
+ description: "",
+ telephoneNumber: "",
+ location: {
+ address: "",
+ city: "",
+ postalCode: "",
+ coordinates: { latitude: 0, longitude: 0 }
+ },
+ cuisine: "",
+ logo: null,
+ coverImage: null,
+ });
+ setTouched({});
+
+ // Store token and restaurant info
+ localStorage.setItem("token", response.data.token);
+ localStorage.setItem("restaurant", JSON.stringify(response.data.restaurant));
+
+ // Redirect to dashboard after successful registration
+ setTimeout(() => {
+ navigate("/dashboard");
+ }, 2000);
+ }
+ } catch (error) {
+ console.error("Registration error:", error);
+
+ // Handle specific error responses from the backend
+ if (error.response?.data?.message) {
+ setMessage(error.response.data.message);
+ } else if (error.response?.status === 400) {
+ setMessage("Registration failed: Invalid information provided");
+ } else if (error.response?.status === 409) {
+ setMessage("An account with this email already exists.");
+ setErrors(prev => ({ ...prev, email: "This email is already registered" }));
+ } else {
+ setMessage("Registration failed. Please try again later.");
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const getPasswordStrengthLabel = () => {
+ if (passwordStrength === 0) return "";
+ if (passwordStrength <= 2) return "Weak";
+ if (passwordStrength <= 4) return "Medium";
+ return "Strong";
+ };
+
+ const getPasswordStrengthClass = () => {
+ if (passwordStrength === 0) return "";
+ if (passwordStrength <= 2) return styles.weakPassword;
+ if (passwordStrength <= 4) return styles.mediumPassword;
+ return styles.strongPassword;
+ };
+
+ return (
+
+
+ Register Restaurant
+
+
+
+ Restaurant Name*
+
+
+ {touched.name && errors.name && (
+
+ {errors.name}
+
+ )}
+
+
+
+
+ Email Address*
+
+
+ {touched.email && errors.email && (
+
+ {errors.email}
+
+ )}
+
+
+
+
+ Password*
+
+
+
+ setShowPassword(!showPassword)}
+ aria-label={showPassword ? "Hide password" : "Show password"}
+ >
+ {showPassword ? "Hide" : "Show"}
+
+
+ {touched.password && errors.password && (
+
+ {errors.password}
+
+ )}
+ {formData.password && (
+
+
+
{getPasswordStrengthLabel()}
+
+ )}
+
+ Password must be at least 8 characters with uppercase & lowercase letters, a number, and a special character.
+
+
+
+
+
+ Confirm Password*
+
+
+
+
+ {touched.confirmPassword && errors.confirmPassword && (
+
+ {errors.confirmPassword}
+
+ )}
+
+
+
+
+ Description*
+
+
+ {touched.description && errors.description && (
+
+ {errors.description}
+
+ )}
+
{formData.description.length}/500
+
+
+
+
+ Telephone Number*
+
+
+ {touched.telephoneNumber && errors.telephoneNumber && (
+
+ {errors.telephoneNumber}
+
+ )}
+
+
+
+
+ Address*
+
+
+ {touched['location.address'] && errors['location.address'] && (
+
+ {errors['location.address']}
+
+ )}
+
+
+
+
+
+ City*
+
+
+ {touched['location.city'] && errors['location.city'] && (
+
+ {errors['location.city']}
+
+ )}
+
+
+
+
+ Postal Code*
+
+
+ {touched['location.postalCode'] && errors['location.postalCode'] && (
+
+ {errors['location.postalCode']}
+
+ )}
+
+
+
+
+
+ Cuisine Types
+
+
+ {touched.cuisine && errors.cuisine && (
+
+ {errors.cuisine}
+
+ )}
+
+
+
+
+ Restaurant Logo*
+
+
setTouched(prev => ({ ...prev, logo: true }))}
+ accept="image/jpeg,image/png,image/gif"
+ aria-required="true"
+ aria-invalid={touched.logo && errors.logo ? "true" : "false"}
+ aria-describedby="logo-error"
+ />
+
+ {formData.logo ? formData.logo.name : 'Choose Logo'}
+
+ {touched.logo && errors.logo && (
+
+ {errors.logo}
+
+ )}
+
Recommended size: 500×500 pixels (Max 5MB)
+
+
+
+
+ Cover Image
+
+
setTouched(prev => ({ ...prev, coverImage: true }))}
+ accept="image/jpeg,image/png,image/gif"
+ aria-invalid={touched.coverImage && errors.coverImage ? "true" : "false"}
+ aria-describedby="cover-error"
+ />
+
+ {formData.coverImage ? formData.coverImage.name : 'Choose Cover Image'}
+
+ {touched.coverImage && errors.coverImage && (
+
+ {errors.coverImage}
+
+ )}
+
Recommended size: 1200×300 pixels (Max 5MB)
+
+
+ {message && (
+
+ {message}
+
+ )}
+
+
+ {isLoading ? 'Registering...' : 'Register Restaurant'}
+
+
+
+ Already have an account? Login here
+
+
+
+ );
+};
+
+export default Register;
\ No newline at end of file
diff --git a/frontend/src/Pages/Restaurant/Register.module.css b/frontend/src/Pages/Restaurant/Register.module.css
new file mode 100644
index 0000000..fa1c7ef
--- /dev/null
+++ b/frontend/src/Pages/Restaurant/Register.module.css
@@ -0,0 +1,406 @@
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes slideIn {
+ from { transform: translateX(-100%); }
+ to { transform: translateX(0); }
+}
+
+@keyframes pulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.02); }
+ 100% { transform: scale(1); }
+}
+
+/* Container variations for light and dark mode */
+.container {
+ min-height: 100vh;
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ transition: all 0.3s ease;
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+}
+
+.dark {
+ background: #121212; /* True dark background */
+}
+
+.form {
+ background: white;
+ width: 100%;
+ max-width: 600px;
+ margin: 0 auto;
+ padding: 2.5rem;
+ border-radius: 15px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+ animation: fadeIn 0.6s ease-out;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s ease;
+}
+
+.dark .form {
+ background: #1e1e1e; /* Darker form background */
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
+}
+
+.title {
+ color: #2c3e50;
+ text-align: center;
+ margin-bottom: 2.5rem;
+ font-size: 2.5rem;
+ font-weight: 700;
+ animation: slideIn 0.8s ease-out;
+ position: relative;
+ transition: color 0.3s ease;
+}
+
+.dark .title {
+ color: #ffffff; /* Pure white text for title in dark mode */
+}
+
+.title:after {
+ content: '';
+ position: absolute;
+ bottom: -10px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 60px;
+ height: 4px;
+ background: #e67e22;
+}
+
+.inputGroup {
+ margin-bottom: 1.8rem;
+ animation: fadeIn 0.6s ease-out;
+ animation-fill-mode: both;
+}
+
+.inputGroup:nth-child(2) { animation-delay: 0.1s; }
+.inputGroup:nth-child(3) { animation-delay: 0.2s; }
+.inputGroup:nth-child(4) { animation-delay: 0.3s; }
+
+.label {
+ display: block;
+ margin-bottom: 0.6rem;
+ color: #2c3e50;
+ font-weight: 600;
+ font-size: 0.95rem;
+ transition: color 0.3s ease;
+}
+
+.dark .label {
+ color: #e0e0e0; /* Light grey for labels in dark mode */
+}
+
+.input {
+ width: 100%;
+ padding: 0.9rem;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ font-size: 1rem;
+ transition: all 0.3s ease;
+ background: #f8fafc;
+}
+
+.dark .input {
+ background: #2a2a2a; /* Dark input background */
+ border-color: #444444; /* Darker border */
+ color: #ffffff; /* White text */
+}
+
+.input:hover {
+ border-color: #bdc3c7;
+}
+
+.dark .input:hover {
+ border-color: #666666; /* Slightly lighter border on hover */
+}
+
+.input:focus {
+ border-color: #e67e22;
+ box-shadow: 0 0 0 3px rgba(230, 126, 34, 0.2);
+ background: white;
+}
+
+.dark .input:focus {
+ border-color: #e67e22;
+ box-shadow: 0 0 0 3px rgba(230, 126, 34, 0.3);
+ background: #333333; /* Slightly lighter than base for focus */
+}
+
+.textarea {
+ composes: input;
+ min-height: 120px;
+ resize: vertical;
+}
+
+.fileInput {
+ display: none;
+}
+
+.fileLabel {
+ display: block;
+ padding: 1rem;
+ background: #f8fafc;
+ border: 2px dashed #bdc3c7;
+ border-radius: 8px;
+ text-align: center;
+ cursor: pointer;
+ color: #7f8c8d;
+ transition: all 0.3s ease;
+ font-weight: 500;
+}
+
+.dark .fileLabel {
+ background: #2a2a2a; /* Dark file input background */
+ border-color: #444444; /* Darker border */
+ color: #b0b0b0; /* Light grey text */
+}
+
+.fileLabel:hover {
+ border-color: #e67e22;
+ color: #e67e22;
+ background: rgba(230, 126, 34, 0.05);
+ transform: translateY(-1px);
+}
+
+.dark .fileLabel:hover {
+ border-color: #e67e22;
+ color: #e67e22;
+ background: rgba(230, 126, 34, 0.1);
+}
+
+.button {
+ width: 100%;
+ padding: 1rem;
+ background: linear-gradient(135deg, #e67e22 0%, #d35400 100%);
+ color: white;
+ 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 rgba(230, 126, 34, 0.3);
+ animation: pulse 1s infinite;
+}
+
+.button:active {
+ transform: translateY(0);
+}
+
+.error {
+ color: #ff6b6b; /* Brighter red for dark mode visibility */
+ 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;
+}
+
+.dark .error {
+ background: rgba(231, 76, 60, 0.15);
+ border: 1px solid rgba(231, 76, 60, 0.3);
+ color: #ff6b6b;
+}
+
+.success {
+ color: #27ae60;
+ 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;
+}
+
+.dark .success {
+ background: rgba(39, 174, 96, 0.15);
+ border: 1px solid rgba(39, 174, 96, 0.3);
+ color: #2ecc71; /* Brighter green for dark mode visibility */
+}
+
+/* Add these new styles to your existing CSS file */
+
+/* Form row for multi-column layout */
+.formRow {
+ display: flex;
+ gap: 1.5rem;
+ margin-bottom: 1.8rem;
+}
+
+.formRow .inputGroup {
+ flex: 1;
+ margin-bottom: 0;
+}
+
+/* Required field indicator */
+.requiredStar {
+ color: #e74c3c;
+ margin-left: 0.2rem;
+}
+
+.dark .requiredStar {
+ color: #ff6b6b;
+}
+
+/* Character counter for textarea */
+.charCounter {
+ font-size: 0.75rem;
+ color: #7f8c8d;
+ text-align: right;
+ margin-top: 0.3rem;
+}
+
+.dark .charCounter {
+ color: #a0a0a0;
+}
+
+/* File hint text */
+.fileHint {
+ font-size: 0.75rem;
+ color: #7f8c8d;
+ margin-top: 0.3rem;
+}
+
+.dark .fileHint {
+ color: #a0a0a0;
+}
+
+/* Password strength meter */
+.passwordInputContainer {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.togglePasswordBtn {
+ position: absolute;
+ right: 10px;
+ background: none;
+ border: none;
+ color: #7f8c8d;
+ font-size: 0.8rem;
+ cursor: pointer;
+ padding: 5px;
+}
+
+.dark .togglePasswordBtn {
+ color: #a0a0a0;
+}
+
+.togglePasswordBtn:hover {
+ color: #e67e22;
+}
+
+.passwordStrength {
+ margin-top: 0.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.strengthMeter {
+ flex: 1;
+ height: 4px;
+ background-color: #e0e0e0;
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.dark .strengthMeter {
+ background-color: #444444;
+}
+
+.strengthIndicator {
+ height: 100%;
+ transition: width 0.3s ease;
+}
+
+.weakPassword {
+ background-color: #e74c3c;
+}
+
+.mediumPassword {
+ background-color: #f39c12;
+}
+
+.strongPassword {
+ background-color: #2ecc71;
+}
+
+.strengthLabel {
+ font-size: 0.75rem;
+ min-width: 50px;
+}
+
+.weakPassword ~ .strengthLabel {
+ color: #e74c3c;
+}
+
+.mediumPassword ~ .strengthLabel {
+ color: #f39c12;
+}
+
+.strongPassword ~ .strengthLabel {
+ color: #2ecc71;
+}
+
+.dark .weakPassword ~ .strengthLabel {
+ color: #ff6b6b;
+}
+
+.dark .mediumPassword ~ .strengthLabel {
+ color: #f5b041;
+}
+
+.dark .strongPassword ~ .strengthLabel {
+ color: #2ecc71;
+}
+
+/* Focus states for accessibility */
+.input:focus, .textarea:focus {
+ outline: none;
+ border-color: #e67e22;
+ box-shadow: 0 0 0 2px rgba(230, 126, 34, 0.3);
+}
+
+.dark .input:focus, .dark .textarea:focus {
+ border-color: #f39c12;
+ box-shadow: 0 0 0 2px rgba(243, 156, 18, 0.3);
+}
+
+/* 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;
+}
+
+/* Responsive layout for form row on mobile */
+@media (max-width: 576px) {
+ .formRow {
+ flex-direction: column;
+ gap: 1.8rem;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/Pages/order/CartPage.jsx b/frontend/src/Pages/order/CartPage.jsx
new file mode 100644
index 0000000..64364ed
--- /dev/null
+++ b/frontend/src/Pages/order/CartPage.jsx
@@ -0,0 +1,185 @@
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+function CartPage() {
+ const [cartItems, setCartItems] = useState([]);
+ const [selectedItems, setSelectedItems] = useState([]);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const cart = JSON.parse(localStorage.getItem('cart')) || [];
+ setCartItems(cart);
+ }, []);
+
+ const handleCheckboxChange = (item) => {
+ const isSelected = selectedItems.some((i) => i._id === item._id);
+ if (isSelected) {
+ setSelectedItems(selectedItems.filter((i) => i._id !== item._id));
+ } else {
+ setSelectedItems([...selectedItems, item]);
+ }
+ };
+
+ // Add function to update quantity
+ const updateQuantity = (itemId, newQuantity) => {
+ // Don't allow quantities less than 1
+ if (newQuantity < 1) return;
+
+ // Update cart items state
+ const updatedCart = cartItems.map(item => {
+ if (item._id === itemId) {
+ return { ...item, quantity: newQuantity };
+ }
+ return item;
+ });
+
+ setCartItems(updatedCart);
+
+ // Update selected items if this item is selected
+ if (selectedItems.some(item => item._id === itemId)) {
+ setSelectedItems(prevSelected =>
+ prevSelected.map(item => {
+ if (item._id === itemId) {
+ return { ...item, quantity: newQuantity };
+ }
+ return item;
+ })
+ );
+ }
+
+ // Update localStorage
+ localStorage.setItem('cart', JSON.stringify(updatedCart));
+ };
+
+ // Add function to remove item from cart
+ const removeFromCart = (itemId) => {
+ const updatedCart = cartItems.filter(item => item._id !== itemId);
+ setCartItems(updatedCart);
+ setSelectedItems(selectedItems.filter(item => item._id !== itemId));
+ localStorage.setItem('cart', JSON.stringify(updatedCart));
+ };
+
+ const totalAmount = selectedItems.reduce((acc, item) => acc + item.price * item.quantity, 0);
+
+ const handleProceedToCheckout = () => {
+ if (selectedItems.length === 0) {
+ alert('Please select at least one item to checkout.');
+ return;
+ }
+ localStorage.setItem('checkoutItems', JSON.stringify(selectedItems));
+ navigate('/checkout');
+ };
+
+ return (
+
+
+
Your Cart
+
+ {cartItems.length === 0 ? (
+
+
Your cart is empty.
+
navigate('/')}
+ className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
+ >
+ Continue Shopping
+
+
+ ) : (
+ <>
+
+ {cartItems.map((item, index) => {
+ const isSelected = selectedItems.some((i) => i._id === item._id);
+ return (
+
+
handleCheckboxChange(item)}
+ className="w-5 h-5 mr-4 accent-blue-500"
+ />
+
+
{item.name}
+
Price: Rs {item.price}
+
+ {/* Quantity controls */}
+
+ Quantity:
+ updateQuantity(item._id, item.quantity - 1)}
+ className="w-8 h-8 flex items-center justify-center bg-gray-200 rounded-l-md hover:bg-gray-300"
+ >
+ -
+
+ updateQuantity(item._id, parseInt(e.target.value) || 1)}
+ className="w-12 h-8 text-center border-y border-gray-200 focus:outline-none"
+ />
+ updateQuantity(item._id, item.quantity + 1)}
+ className="w-8 h-8 flex items-center justify-center bg-gray-200 rounded-r-md hover:bg-gray-300"
+ >
+ +
+
+
+
+
Subtotal: Rs {item.price * item.quantity}
+
+
+ {/* Remove button */}
+
removeFromCart(item._id)}
+ className="ml-4 p-2 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-full"
+ title="Remove item"
+ >
+
+
+
+
+
+ );
+ })}
+
+
+
+
+ Total for selected: Rs {totalAmount}
+
+
+
+ navigate('/')}
+ className="w-full md:w-auto px-6 py-3 text-blue-600 border border-blue-600 rounded-lg hover:bg-blue-50 transition-colors"
+ >
+ Continue Shopping
+
+
+
+ Proceed to Checkout
+
+
+
+ >
+ )}
+
+
+ );
+}
+
+export default CartPage;
diff --git a/frontend/src/Pages/order/Checkoutpage.jsx b/frontend/src/Pages/order/Checkoutpage.jsx
new file mode 100644
index 0000000..938e690
--- /dev/null
+++ b/frontend/src/Pages/order/Checkoutpage.jsx
@@ -0,0 +1,218 @@
+import { useEffect, useState } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+import axios from 'axios'
+
+function CheckoutPage() {
+ const [checkoutItems, setCheckoutItems] = useState([])
+ const [paymentMethod, setPaymentMethod] = useState('cash')
+ const [address, setAddress] = useState('')
+ const [phone, setPhone] = useState('')
+ const [loading, setLoading] = useState(false)
+ const navigate = useNavigate()
+
+ useEffect(() => {
+ const items = JSON.parse(localStorage.getItem('checkoutItems')) || []
+ setCheckoutItems(items)
+ }, [])
+
+ const totalAmount = checkoutItems.reduce(
+ (acc, item) => acc + item.price * item.quantity,
+ 0
+ )
+
+ const handlePlaceOrder = async () => {
+ if (!address.trim() || !phone.trim()) {
+ alert('Please fill address and phone number.')
+ return
+ }
+
+ if (checkoutItems.length === 0) {
+ alert('No items selected for checkout.')
+ return
+ }
+
+ setLoading(true)
+
+ try {
+ // Format items the same way as in PaymentPage
+ const orderItems = checkoutItems.map((item) => ({
+ productId: item._id,
+ quantity: item.quantity,
+ }))
+
+ if (paymentMethod === 'cash') {
+ // Cash on delivery order creation
+ const response = await axios.post('/api/orders', {
+ userId: 'user-123',
+ items: orderItems.map((item) => ({
+ ...item,
+ // Retrieve restaurantId from localStorage or use default
+ restaurantId:
+ localStorage.getItem(
+ `restaurant_${item.productId}`
+ ) || 'default-restaurant',
+ })),
+ paymentMethod,
+ currency: 'usd',
+ deliveryAddress: address,
+ phoneNumber: phone,
+ // Note: When using cash payment, backend calculates totalAmount
+ })
+
+ console.log('Order created:', response.data)
+ alert('Order placed successfully!')
+ localStorage.removeItem('cart')
+ localStorage.removeItem('checkoutItems')
+ navigate('/')
+ } else if (paymentMethod === 'card') {
+ // Create payment intent first
+ const paymentResponse = await axios.post(
+ '/api/payments/create-payment-intent',
+ {
+ amount: Math.round(totalAmount * 100), // Convert to cents and ensure it's an integer
+ currency: 'usd',
+ }
+ )
+
+ // Store necessary data for payment page
+ localStorage.setItem('paymentItems', JSON.stringify(orderItems))
+ localStorage.setItem('paymentAddress', address)
+ localStorage.setItem('paymentPhone', phone)
+ localStorage.setItem('paymentMethod', paymentMethod)
+ localStorage.setItem(
+ 'paymentClientSecret',
+ paymentResponse.data.clientSecret
+ )
+
+ navigate('/payment')
+ }
+ } catch (error) {
+ console.error('Error placing order:', error)
+
+ if (error.response) {
+ console.error('Response data:', error.response.data)
+ console.error('Response status:', error.response.status)
+ alert(
+ `Error: ${error.response.data.message || 'Something went wrong. Please try again.'}`
+ )
+ } else {
+ alert('Something went wrong. Please try again.')
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Checkout
+
+
+ {checkoutItems.length === 0 ? (
+
+
No items to checkout.
+
+ ) : (
+ <>
+
+ {checkoutItems.map((item, index) => (
+
+
+ {item.name}
+
+
+ Price: Rs {item.price}
+
+
+ Quantity: {item.quantity}
+
+
+ Subtotal: Rs{' '}
+ {item.price * item.quantity}
+
+
+ ))}
+
+
+
+
+ Total Amount: Rs {totalAmount}
+
+
+
+
+
+ Delivery Address:
+
+
+ setAddress(e.target.value)
+ }
+ rows="3"
+ placeholder="Enter your address"
+ className="w-full rounded-lg border-gray-300 p-3 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+
+
+
+
+ Phone Number:
+
+
+ setPhone(e.target.value)
+ }
+ placeholder="Enter phone number"
+ className="w-full rounded-lg border-gray-300 p-3 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+
+
+
+
+ Payment Method:
+
+
+ setPaymentMethod(e.target.value)
+ }
+ className="w-full rounded-lg border-gray-300 p-3 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ >
+
+ Cash on Delivery
+
+
+ Card Payment
+
+
+
+
+
+
+ {loading
+ ? 'Placing Order...'
+ : 'Place Order'}
+
+
+
+
+ >
+ )}
+
+
+ )
+}
+
+export default CheckoutPage
diff --git a/frontend/src/Pages/order/HomePage.jsx b/frontend/src/Pages/order/HomePage.jsx
new file mode 100644
index 0000000..2e1a1f1
--- /dev/null
+++ b/frontend/src/Pages/order/HomePage.jsx
@@ -0,0 +1,171 @@
+import { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import axios from 'axios'
+
+function HomePage() {
+ const [products, setProducts] = useState([])
+ const [isLoading, setIsLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [cartItemsCount, setCartItemsCount] = useState(0)
+ const navigate = useNavigate()
+
+ useEffect(() => {
+ const fetchProducts = async () => {
+ try {
+ setIsLoading(true)
+ const res = await axios.get('/api/products')
+ setProducts(res.data)
+ } catch (err) {
+ console.error('Failed to fetch products', err)
+ setError('Failed to load products. Please try again later.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ fetchProducts()
+ }, [])
+
+ const handleViewDetails = (id) => {
+ navigate(`/products/${id}`)
+ }
+
+ const handleNavigateToCart = () => {
+ navigate('/cart')
+ }
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (error) {
+ return (
+
+
+
{error}
+
window.location.reload()}
+ className="mt-2 rounded bg-red-500 px-4 py-2 font-bold text-white hover:bg-red-700"
+ >
+ Retry
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Navigation with Cart Button */}
+
+
+
+
+
+ ShopEase
+
+
+
+
+
+
+
+ {cartItemsCount > 0 && (
+
+ {cartItemsCount}
+
+ )}
+
+
+
+
+
+
+ {/* Main Content */}
+
+
+
+
+ Our Products
+
+
+ Discover our premium collection
+
+
+
+
+ {products.map((product) => (
+
+
+ {product.image ? (
+
+ ) : (
+
+ No image available
+
+ )}
+
+
+
+ {product.name}
+
+
+ {product.description}
+
+
+
+ Rs {product.price.toLocaleString()}
+
+
+ handleViewDetails(product._id)
+ }
+ className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
+ >
+ View Details
+
+
+
+
+ ))}
+
+
+ {products.length === 0 && !isLoading && (
+
+
+ No products available at the moment.
+
+
+ )}
+
+
+
+ )
+}
+
+export default HomePage
diff --git a/frontend/src/Pages/order/PaymentPage.jsx b/frontend/src/Pages/order/PaymentPage.jsx
new file mode 100644
index 0000000..ba16ccd
--- /dev/null
+++ b/frontend/src/Pages/order/PaymentPage.jsx
@@ -0,0 +1,179 @@
+import React, { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { loadStripe } from '@stripe/stripe-js'
+import {
+ Elements,
+ useStripe,
+ useElements,
+ CardElement,
+} from '@stripe/react-stripe-js'
+import axios from 'axios'
+
+const stripePromise = loadStripe(
+ 'pk_test_51RErI8RaMVEyYN7k9JSNZHcktzyVq8fybSIc3KZshl2I2Iy5Q8VJcGgp716TC4MzTKySp7xsy5lrbVMh9gb0s0wb00dvEjXEc2'
+)
+
+function CheckoutForm({ clientSecret }) {
+ const stripe = useStripe()
+ const elements = useElements()
+ const navigate = useNavigate()
+ const [loading, setLoading] = useState(false)
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+ if (!stripe || !elements) return
+
+ setLoading(true)
+
+ try {
+ const { error, paymentIntent } = await stripe.confirmCardPayment(
+ clientSecret,
+ {
+ payment_method: {
+ card: elements.getElement(CardElement),
+ },
+ }
+ )
+
+ if (error) {
+ console.error('Stripe error:', error.message)
+ alert(error.message)
+ setLoading(false)
+ return
+ }
+
+ if (paymentIntent.status === 'succeeded') {
+ try {
+ // Get stored order details from localStorage
+ const rawItems = JSON.parse(
+ localStorage.getItem('paymentItems')
+ )
+ const address = localStorage.getItem('paymentAddress')
+ const phone = localStorage.getItem('paymentPhone')
+ const paymentMethod = 'card' // Always card in the payment page
+
+ // Format items properly for backend
+ const formattedItems = rawItems.map((item) => ({
+ productId: item.productId,
+ quantity: item.quantity,
+ }))
+
+ console.log(
+ 'Payment successful, creating order with data:',
+ {
+ userId: 'user-123',
+ items: formattedItems,
+ totalAmount: paymentIntent.amount / 100,
+ paymentMethod,
+ deliveryAddress: address,
+ phoneNumber: phone,
+ }
+ )
+
+ // Create order in backend
+ const response = await axios.post('/api/orders', {
+ userId: 'user-123',
+ items: formattedItems.map((item) => ({
+ ...item,
+ // Retrieve restaurantId from localStorage or use default
+ restaurantId:
+ localStorage.getItem(
+ `restaurant_${item.productId}`
+ ) || 'default-restaurant',
+ })),
+ totalAmount: paymentIntent.amount / 100,
+ paymentMethod,
+ currency: paymentIntent.currency,
+ deliveryAddress: address,
+ phoneNumber: phone,
+ })
+
+ console.log('Order created successfully:', response.data)
+
+ // Clear all localStorage items
+ ;[
+ 'cart',
+ 'checkoutItems',
+ 'paymentClientSecret',
+ 'paymentItems',
+ 'paymentAddress',
+ 'paymentPhone',
+ 'paymentMethod',
+ ].forEach((key) => localStorage.removeItem(key))
+
+ alert('Payment and Order Successful!')
+ navigate('/')
+ } catch (error) {
+ console.error('Order creation failed after payment:', error)
+
+ if (error.response) {
+ console.error('Response data:', error.response.data)
+ console.error('Response status:', error.response.status)
+ console.error(
+ 'Request that was sent:',
+ error.config.data
+ )
+
+ alert(
+ `Payment successful but order creation failed: ${error.response.data.message || error.message}`
+ )
+ } else {
+ alert(
+ `Payment successful but order creation failed: ${error.message}`
+ )
+ }
+ }
+ }
+ } catch (stripeError) {
+ console.error('Unexpected Stripe error:', stripeError)
+ alert(`Payment processing error: ${stripeError.message}`)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
Complete Payment
+
+
+
+ {loading ? 'Processing...' : 'Pay Now'}
+
+
+
+ )
+}
+
+function PaymentPage() {
+ const [clientSecret, setClientSecret] = useState('')
+ const navigate = useNavigate()
+
+ useEffect(() => {
+ const secret = localStorage.getItem('paymentClientSecret')
+ if (!secret) {
+ alert('No payment client secret found.')
+ navigate('/checkout')
+ } else {
+ setClientSecret(secret)
+ }
+ }, [navigate])
+
+ if (!clientSecret) {
+ return
Loading Payment...
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default PaymentPage
diff --git a/frontend/src/Pages/order/ProductDetails.jsx b/frontend/src/Pages/order/ProductDetails.jsx
new file mode 100644
index 0000000..9b794aa
--- /dev/null
+++ b/frontend/src/Pages/order/ProductDetails.jsx
@@ -0,0 +1,213 @@
+import { useParams, useNavigate } from 'react-router-dom';
+import { useEffect, useState, useCallback } from 'react';
+import axios from 'axios';
+
+function ProductDetailsPage() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const [product, setProduct] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [isAddingToCart, setIsAddingToCart] = useState(false);
+
+ // Add this state for quantity
+ const [quantity, setQuantity] = useState(1);
+
+ // Memoized fetch function
+ const fetchProduct = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ const res = await axios.get(`/api/products/${id}`);
+ setProduct(res.data);
+ } catch (err) {
+ console.error('Failed to fetch product', err);
+ setError('Failed to load product details. Please try again later.');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [id]);
+
+ useEffect(() => {
+ fetchProduct();
+ }, [fetchProduct]);
+
+ const handleAddToCart = async () => {
+ setIsAddingToCart(true);
+
+ try {
+ let cart = JSON.parse(localStorage.getItem('cart')) || [];
+ const existingItemIndex = cart.findIndex((item) => item._id === product._id);
+
+ if (existingItemIndex !== -1) {
+ // If product already in cart, add the new quantity to the existing one
+ cart[existingItemIndex].quantity += quantity;
+ } else {
+ // Otherwise add new product with selected quantity
+ cart.push({ ...product, quantity });
+ }
+
+ localStorage.setItem('cart', JSON.stringify(cart));
+
+ // Store restaurantId for this product separately for easy access when creating orders
+ localStorage.setItem(`restaurant_${product._id}`, product.restaurantId || 'default-restaurant');
+
+ alert(`${product.name} added to cart!`);
+ navigate('/cart');
+ } catch (err) {
+ alert('Failed to add to cart');
+ console.error(err);
+ } finally {
+ setIsAddingToCart(false);
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
{error}
+
window.location.reload()}
+ className="mt-2 bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
+ >
+ Retry
+
+
+
+ );
+ }
+
+ if (!product) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
navigate(-1)}
+ className="mb-6 flex items-center text-blue-600 hover:text-blue-800 transition-colors"
+ >
+
+
+
+ Back to products
+
+
+
+ {/* Product Image */}
+
+ {product.image ? (
+
+ ) : (
+
+
+
+
+
No image available
+
+ )}
+
+
+ {/* Product Details */}
+
+
{product.name}
+
+
+
+ {[...Array(5)].map((_, i) => (
+
+
+
+ ))}
+
+
(24 reviews)
+
+
+
+ Rs {product.price.toLocaleString()}
+
+
+
{product.description}
+
+
+
Details
+
+ High-quality materials
+ Eco-friendly packaging
+ 30-day return policy
+
+
+
+ {/* Add this quantity selector */}
+
+
Quantity
+
+ setQuantity(prev => Math.max(1, prev - 1))}
+ className="px-4 py-2 border border-gray-300 rounded-l-md bg-gray-100 hover:bg-gray-200"
+ >
+ -
+
+ setQuantity(Math.max(1, parseInt(e.target.value) || 1))}
+ className="w-16 text-center py-2 border-t border-b border-gray-300"
+ />
+ setQuantity(prev => prev + 1)}
+ className="px-4 py-2 border border-gray-300 rounded-r-md bg-gray-100 hover:bg-gray-200"
+ >
+ +
+
+
+
+
+
+ {isAddingToCart ? (
+
+
+
+
+
+ Adding...
+
+ ) : (
+ 'Add to Cart'
+ )}
+
+
+
+
+
+ );
+}
+
+export default ProductDetailsPage;
\ No newline at end of file
diff --git a/frontend/src/Pages/order/SuccessPage.jsx b/frontend/src/Pages/order/SuccessPage.jsx
new file mode 100644
index 0000000..5354775
--- /dev/null
+++ b/frontend/src/Pages/order/SuccessPage.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+
+const SuccessPage = () => {
+ return (
+
+
🎉 Order Successful!
+
Thank you for your purchase.
+
+ );
+};
+
+export default SuccessPage;
diff --git a/frontend/src/api/orderApi.js b/frontend/src/api/orderApi.js
new file mode 100644
index 0000000..397254d
--- /dev/null
+++ b/frontend/src/api/orderApi.js
@@ -0,0 +1,5 @@
+import axios from 'axios';
+
+const API_URL = 'http://localhost:5000/api';
+
+export const createOrder = (orderData) => axios.post(`${API_URL}/orders`, orderData);
diff --git a/frontend/src/api/productApi.js b/frontend/src/api/productApi.js
new file mode 100644
index 0000000..ef44608
--- /dev/null
+++ b/frontend/src/api/productApi.js
@@ -0,0 +1,6 @@
+import axios from 'axios';
+
+const API_URL ='http://localhost:5000/api';
+
+export const getAllProducts = () => axios.get(`${API_URL}/products`);
+export const getProductById = (id) => axios.get(`${API_URL}/products/${id}`);
diff --git a/frontend/src/restaurant.css b/frontend/src/restaurant.css
new file mode 100644
index 0000000..25ed432
--- /dev/null
+++ b/frontend/src/restaurant.css
@@ -0,0 +1,98 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ /* Light theme (default) */
+ --background-color: #f5f7fa;
+ --text-color: #2c3e50;
+ --text-secondary: #7f8c8d;
+ --primary-color: #e67e22;
+ --primary-color-rgb: 230, 126, 34;
+ --secondary-color: #d35400;
+ --secondary-color-rgb: 211, 84, 0;
+ --card-background: #ffffff;
+ --border-color: #e0e0e0;
+ --input-background: #f8fafc;
+ --hover-background: #f8f9fa;
+ --shadow-color: rgba(0, 0, 0, 0.1);
+ --success-color: #27ae60;
+ --success-color-rgb: 39, 174, 96;
+ --error-color: #e74c3c;
+ --error-color-rgb: 231, 76, 60;
+ --tag-background: #fdf2e9;
+ --tag-text: #e67e22;
+ --button-text: #ffffff;
+ --notification-shadow: rgba(0, 0, 0, 0.1);
+ --tooltip-background: rgba(0, 0, 0, 0.8);
+ --tooltip-text: #ffffff;
+ --focus-outline: rgba(230, 126, 34, 0.4);
+}
+
+body {
+ background-color: var(--background-color);
+ color: var(--text-color);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ transition: background-color 0.3s, color 0.3s;
+}
+
+body.dark-mode {
+ /* Dark theme */
+ --background-color: #121212;
+ --text-color: #ecf0f1;
+ --text-secondary: #bdc3c7;
+ --primary-color: #f39c12;
+ --primary-color-rgb: 243, 156, 18;
+ --secondary-color: #e67e22;
+ --secondary-color-rgb: 230, 126, 34;
+ --card-background: #1e1e1e;
+ --border-color: #333333;
+ --input-background: #2c2c2c;
+ --hover-background: #2a2a2a;
+ --shadow-color: rgba(0, 0, 0, 0.3);
+ --success-color: #2ecc71;
+ --success-color-rgb: 46, 204, 113;
+ --error-color: #e74c3c;
+ --error-color-rgb: 231, 76, 60;
+ --tag-background: #3d3112;
+ --tag-text: #f39c12;
+ --button-text: #ffffff;
+ --notification-shadow: rgba(0, 0, 0, 0.4);
+ --tooltip-background: rgba(255, 255, 255, 0.8);
+ --tooltip-text: #121212;
+ --focus-outline: rgba(243, 156, 18, 0.4);
+}
+
+/* Add some scrollbar styling for dark mode */
+body.dark-mode::-webkit-scrollbar {
+ width: 12px;
+}
+
+body.dark-mode::-webkit-scrollbar-track {
+ background: #1e1e1e;
+}
+
+body.dark-mode::-webkit-scrollbar-thumb {
+ background-color: #333;
+ border-radius: 6px;
+ border: 3px solid #1e1e1e;
+}
+
+/* Transition for all elements */
+* {
+ transition: background-color 0.3s, color 0.3s, border-color 0.3s, box-shadow 0.3s;
+
+}
+
+/* Form input focus styling */
+input:focus, textarea:focus, select:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px var(--focus-outline);
+}
+
+/* Dark mode image adjustment */
+body.dark-mode img {
+ filter: brightness(0.9);
+}
\ No newline at end of file
diff --git a/frontend/src/services/Stripe.jsx b/frontend/src/services/Stripe.jsx
new file mode 100644
index 0000000..b7d462e
--- /dev/null
+++ b/frontend/src/services/Stripe.jsx
@@ -0,0 +1,11 @@
+import { loadStripe } from '@stripe/stripe-js';
+import { Elements } from '@stripe/react-stripe-js';
+import CheckoutForm from '../components/CheckoutForm';
+import React from 'react';
+
+const PUBLISH_KEY = 'pk_test_51RErI8RaMVEyYN7k9JSNZHcktzyVq8fybSIc3KZshl2I2Iy5Q8VJcGgp716TC4MzTKySp7xsy5lrbVMh9gb0s0wb00dvEjXEc2';
+const stripePromise = loadStripe(PUBLISH_KEY);
+
+
+
+
diff --git a/frontend/src/services/localStorageUtils.jsx b/frontend/src/services/localStorageUtils.jsx
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 40df252..936a446 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -552,6 +552,18 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz#fd92d31a2931483c25677b9c6698106490cbbc76"
integrity sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==
+"@stripe/react-stripe-js@^3.6.0":
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-3.6.0.tgz#af6b66c006eef97ed885815ae976fb74f0aa1bf4"
+ integrity sha512-zEnaUmTOsu7zhl3RWbZ0l1dRiad+QIbcAYzQfF+yYelURJowhAwesRHKWH+qGAIBEpkO6/VCLFHhVLH9DtPlnw==
+ dependencies:
+ prop-types "^15.7.2"
+
+"@stripe/stripe-js@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-7.2.0.tgz#f5a256f27f946ccf9cc8c2cc4b34fc40079dbfca"
+ integrity sha512-BXlt6BsFE599yOATuz78FiW9z4SyipCH3j1SDyKWe/3OUBdhcOr/BWnBi1xrtcC2kfqrTJjgvfH2PTfMPRmbTw==
+
"@tailwindcss/node@4.1.4":
version "4.1.4"
resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.1.4.tgz#cfabbbcd53cbbae8a175dc744e6fe31e8ad43d3e"
@@ -774,10 +786,10 @@ axios-mock-adapter@^2.1.0:
fast-deep-equal "^3.1.3"
is-buffer "^2.0.5"
-axios@^1.8.4:
- version "1.8.4"
- resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447"
- integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==
+axios@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.9.0.tgz#25534e3b72b54540077d33046f77e3b8d7081901"
+ integrity sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
@@ -871,6 +883,14 @@ cookie@^1.0.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610"
integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
+cors@^2.8.5:
+ version "2.8.5"
+ resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
+ integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
+ dependencies:
+ object-assign "^4"
+ vary "^1"
+
cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -1307,7 +1327,7 @@ jiti@^2.4.2:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560"
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==
-js-tokens@^4.0.0:
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
@@ -1439,6 +1459,13 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+loose-envify@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -1490,6 +1517,11 @@ node-releases@^2.0.19:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
+object-assign@^4, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
optionator@^0.9.3:
version "0.9.4"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
@@ -1562,6 +1594,15 @@ prettier@^3.5.3:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5"
integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==
+prop-types@^15.7.2:
+ version "15.8.1"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+ integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.13.1"
+
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
@@ -1592,6 +1633,11 @@ react-icons@^5.5.0:
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.5.0.tgz#8aa25d3543ff84231685d3331164c00299cdfaf2"
integrity sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==
+react-is@^16.13.1:
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+ integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
react-refresh@^0.17.0:
version "0.17.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.17.0.tgz#b7e579c3657f23d04eccbe4ad2e58a8ed51e7e53"
@@ -1758,6 +1804,11 @@ use-sync-external-store@^1.4.0:
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
+vary@^1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+
vite@^6.2.0:
version "6.2.6"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.2.6.tgz#7f0ccf2fdc0c1eda079ce258508728e2473d3f61"
diff --git a/k8s/kustomization/README.md b/k8s/kustomization/README.md
new file mode 100644
index 0000000..0610db9
--- /dev/null
+++ b/k8s/kustomization/README.md
@@ -0,0 +1,142 @@
+# Deployment Guide
+
+- [Deployment Guide](#deployment-guide)
+ - [0. Directory Structute](#0-directory-structute)
+ - [1. Environment Configuration](#1-environment-configuration)
+ - [2. ConfigMap Generation](#2-configmap-generation)
+ - [3. Deploy the Application](#3-deploy-the-application)
+ - [4. PostgreSQL Installation (via Helm)](#4-postgresql-installation-via-helm)
+ - [Step 1: Add Bitnami Repository](#step-1-add-bitnami-repository)
+ - [Step 2: Install PostgreSQL](#step-2-install-postgresql)
+ - [Step 3: Verify Installation](#step-3-verify-installation)
+ - [5. Notes](#5-notes)
+
+## 0. Directory Structute
+
+```plaintext
+.
+├── base
+│ ├── configs
+│ │ ├── email-service.env
+│ │ ├── notification-service.env
+│ │ ├── order-service.env
+│ │ ├── payment-service.env
+│ │ ├── sms-service.env
+│ │ └── user-service.env
+│ ├── deployments
+│ │ ├── email-service-deployment.yaml
+│ │ ├── frontend-service-deployment.yaml
+│ │ ├── notification-service-deployment.yaml
+│ │ ├── order-service-deployment.yaml
+│ │ ├── payment-service-deployment.yaml
+│ │ ├── sms-service-deployment.yaml
+│ │ └── user-service-deployment.yaml
+│ ├── ingress
+│ │ └── ingress.yaml
+│ ├── kustomization.yaml
+│ └── rabbitmq
+│ └── rabbitmq.yaml
+└── README.md
+```
+
+## 1. Environment Configuration
+
+Before deploying, copy the environment files to the `configs/` directory and rename them properly if needed.
+
+```bash
+# Example
+cp email-service/.env base/configs/email-service.env
+cp notification-service/.env base/configs/notification-service.env
+cp order-service/.env base/configs/order-service.env
+cp payment-service/.env base/configs/payment-service.env
+cp sms-service/.env base/configs/sms-service.env
+cp user-service/.env base/configs/user-service.env
+```
+
+Make sure each service has the correct `.env` file placed inside `base/configs/`.
+
+---
+
+## 2. ConfigMap Generation
+
+Kustomize automatically **generates ConfigMaps** from the files under `base/configs/` based on the `kustomization.yaml` configuration.
+You **do not need to manually create ConfigMaps** — they are created during `kubectl apply -k`.
+
+> Learn more: [Kustomize ConfigMap Generation](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/configmapgenerator/)
+
+Example snippet from `kustomization.yaml`:
+
+```yaml
+configMapGenerator:
+ - name: user-service-config
+ envs:
+ - configs/user-service.env
+ - name: email-service-config
+ envs:
+ - configs/email-service.env
+ # (other services...)
+```
+---
+
+## 3. Deploy the Application
+
+Apply the full base configuration to your cluster:
+
+```bash
+kubectl apply -k k8s/base
+```
+
+This will deploy:
+- Microservices (User, Order, Payment, Notification, Email, SMS)
+- Frontend
+- RabbitMQ
+- Ingress
+
+---
+
+## 4. PostgreSQL Installation (via Helm)
+
+Install **PostgreSQL** using the Bitnami Helm chart:
+
+### Step 1: Add Bitnami Repository
+
+```bash
+helm repo add bitnami https://charts.bitnami.com/bitnami
+helm repo update
+```
+
+### Step 2: Install PostgreSQL
+
+```bash
+helm install postgres-release bitnami/postgresql \
+ --set auth.username=youruser \
+ --set auth.password=yourpassword \
+ --set auth.database=yourdatabase
+```
+
+🔵 **Replace** `youruser`, `yourpassword`, and `yourdatabase` with your actual credentials.
+
+You can also customize values further using a `values.yaml` file if needed.
+
+### Step 3: Verify Installation
+
+```bash
+kubectl get pods
+kubectl get svc
+```
+
+Make sure the PostgreSQL pod and service are running!
+
+> Full chart documentation: [Bitnami PostgreSQL Helm Chart](https://artifacthub.io/packages/helm/bitnami/postgresql)
+
+---
+
+## 5. Notes
+
+- Make sure RabbitMQ is running first if services depend on it.
+- If needed, update `ingress/ingress.yaml` according to your domain or host settings.
+- Make sure your Kubernetes cluster has an Ingress Controller (like NGINX) installed.
+
+
+
+
diff --git a/k8s/kustomization/base/deployments/order-service-deployment.yaml b/k8s/kustomization/base/deployments/order-service-deployment.yaml
new file mode 100644
index 0000000..0757b49
--- /dev/null
+++ b/k8s/kustomization/base/deployments/order-service-deployment.yaml
@@ -0,0 +1,42 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: order-service
+ namespace: default
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: order-service
+ template:
+ metadata:
+ labels:
+ app: order-service
+ spec:
+ containers:
+ - name: order-service
+ image: nmdra/order-service:latest
+ imagePullPolicy: Never
+ ports:
+ - containerPort: 5000
+ env:
+ - name: NODE_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: spec.nodeName
+ envFrom:
+ - configMapRef:
+ name: order-service-env
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: order-service
+ namespace: default
+spec:
+ selector:
+ app: order-service
+ type: ClusterIP
+ ports:
+ - port: 80
+ targetPort: 5000
diff --git a/k8s/kustomization/base/deployments/payment-service-deployment.yaml b/k8s/kustomization/base/deployments/payment-service-deployment.yaml
new file mode 100644
index 0000000..bbdeea9
--- /dev/null
+++ b/k8s/kustomization/base/deployments/payment-service-deployment.yaml
@@ -0,0 +1,42 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: payment-service
+ namespace: default
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: payment-service
+ template:
+ metadata:
+ labels:
+ app: payment-service
+ spec:
+ containers:
+ - name: payment-service
+ image: nmdra/payment-service:latest
+ imagePullPolicy: Never
+ ports:
+ - containerPort: 5000
+ env:
+ - name: NODE_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: spec.nodeName
+ envFrom:
+ - configMapRef:
+ name: payment-service-env
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: payment-service
+ namespace: default
+spec:
+ selector:
+ app: payment-service
+ type: ClusterIP
+ ports:
+ - port: 80
+ targetPort: 5000
diff --git a/k8s/kustomization/base/ingress/ingress.yaml b/k8s/kustomization/base/ingress/ingress.yaml
index 4c5c160..0cbde0c 100644
--- a/k8s/kustomization/base/ingress/ingress.yaml
+++ b/k8s/kustomization/base/ingress/ingress.yaml
@@ -28,6 +28,30 @@ spec:
port:
number: 80
+ - path: /api/orders(/|$)(.*)
+ pathType: ImplementationSpecific
+ backend:
+ service:
+ name: order-service
+ port:
+ number: 80
+
+ - path: /api/products(/|$)(.*)
+ pathType: ImplementationSpecific
+ backend:
+ service:
+ name: order-service
+ port:
+ number: 80
+
+ - path: /api/payments(/|$)(.*)
+ pathType: ImplementationSpecific
+ backend:
+ service:
+ name: payment-service
+ port:
+ number: 80
+
- path: /health(/|$)(.*)
pathType: ImplementationSpecific
backend:
diff --git a/k8s/kustomization/base/kustomization.yaml b/k8s/kustomization/base/kustomization.yaml
index 6f85af8..abb8c1c 100644
--- a/k8s/kustomization/base/kustomization.yaml
+++ b/k8s/kustomization/base/kustomization.yaml
@@ -3,6 +3,8 @@ resources:
- deployments/notification-service-deployment.yaml
- deployments/sms-service-deployment.yaml
- deployments/user-service-deployment.yaml
+ - deployments/order-service-deployment.yaml
+ - deployments/payment-service-deployment.yaml
- deployments/frontend-service-deployment.yaml
- rabbitmq/rabbitmq.yaml
- ingress/ingress.yaml
@@ -20,6 +22,12 @@ configMapGenerator:
- name: user-service-env
envs:
- configs/user-service.env
+ - name: order-service-env
+ envs:
+ - configs/order-service.env
+ - name: payment-service-env
+ envs:
+ - configs/payment-service.env
generatorOptions:
disableNameSuffixHash: true
diff --git a/k8s/local-cluster/config b/k8s/local-cluster/config
new file mode 100644
index 0000000..abd5543
--- /dev/null
+++ b/k8s/local-cluster/config
@@ -0,0 +1,19 @@
+apiVersion: v1
+clusters:
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJVlNYRXB4eFpHV2d3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRBME1qZ3dORFF3TlRoYUZ3MHpOVEEwTWpZd05EUTFOVGhhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURDWnNDZU0yTTRDOU5yeTRwNzV5M29TNHlsY2VhZ2NDaE1pMjhqb1g3VmlXdDZXRmQ4a1JqeUZ2aDMKdXlyL3BJUWlDMk1KR2ZGeVZVZUhIcFBpdzBVcWNzUDBtcFJWU0JZOGR0M1dxY0dJY0dRSkdMd1JqeFhuVlZITwp6VVhNM2V5N2lqZkZlSHM1WmIwbGdxTlpodWRNUVh1ZnVQMHFtVnl1NTBoTTU5amFWQkUwTUJaNENRZk5DRUNxCkZ3aWNOR2REbUZCK1NEU1YwZTd2b0lCKzhBZ3Q5dDFVU2I4OG80cTE1aFNCUU5vWUVkUEhBTFRVMlNjZlhhL3AKV04xdDJmUDM5Wm5XeWxCcm1FR1Zub29LSU9sS2l2UHBwK0g1bzRvK29BVlFKc2xUQTZBeU9vWUh6Tnhmd2xQTwo5dmFvcG5wUURLSm9OWkpvcDYvb3VMeE9PbmFWQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJSSlBWbkMzOWVLZ1JiNTRtMXUvOFNpTXd1T1lqQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ3JCTWRhUWc3cQpRL1hhTTBlVjFVOUJ5VTVDMllDWTYrcnI1TTZDQ2w5c1lSQ0VrbGI0cG5VL1Zjemw4VFFTSm9VeGNkUHl5RWJPCldDZUtNQXFkR0lqL1U5dGtpMlFlVzUzdXpEVXp4QkRZTmhvdFFXQ0xUTmF1L3lXSENKMm9zYTJJclY4WEVZZmUKRmQ0aWNOZnF3eUh6QjRxZWpodC9aWlo0L3BFOXdJRGd0K0NVNUNEMHFMNm1VMDVQcFhRZ245a0JoazVvRVdlRwpENHgvOGVOYTQ5ZHcwb1VrNFdtaVEvQUpCeGEvM2J5Wng4aWhUSnBuZGJjT3JDVWJaNjdEV1ZtSmlWeEJXVGc3CjJaOUR5Wmh1RDR5Wk5ZTTVtcDRjeUw5K2lzOXBXZklxZy8ydUl6RmRPTEM1L1hLWjNJdVBSWWpFYTZGQzMzejEKQTVYeDhxOFdiN2JhCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
+ server: https://127.0.0.1:46819
+ name: kind-kind
+contexts:
+- context:
+ cluster: kind-kind
+ user: kind-kind
+ name: kind-kind
+current-context: kind-kind
+kind: Config
+preferences: {}
+users:
+- name: kind-kind
+ user:
+ client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJYnU1Z3U1dy8vRjR3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRBME1qZ3dORFF3TlRoYUZ3MHlOakEwTWpnd05EUTFOVGxhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEZE04OFgKMGg4Z1NHNGVraFdoY212Vmp4eXBUSXI0QTF2RE1XME1wc0p2by8vZDdFMmwwbTk2UnhRQ3pwRnlWMXFXT0J1LwowRDFGdXFwckxRblQwYUZJYXg3TTAzQ1IwSytUd2NFNXg3bHFkL3I0NC9CL3BzQ3ZCOGRIMGdxZGJpOHdEWTkyCks2UnlBOVIyelZEcjZpSmRTZWtzU0M3K3VCWS82Q1l3QjQ0Mk5GKzZXaWxMRzRDSTRRVFZLN3ZEMVByRjhUaFUKMng5a3FpNVd6b1J6TXFoN3RCRFJMbzJiTTBubVVNL2x0RGIvMHlsYVRKZFB3ajQzbnJGN0lsWEh0Ujl1S21INQprdSt3MFFOVXI2d1hVQmlFL01uZkZ2V01iZ0l4ZzJ5aHNnQlV4d1QrcFFpUXdITlFtZlFUVTFuV21mZ0ZPZXB3Cmlwd1Fnd2hFZ1gxOFZpMVJBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkVrOVdjTGYxNHFCRnZuaQpiVzcveEtJekM0NWlNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFFaDB5ZlRsditJdmVsMHNxTXpteDFiMzM3CkYzdWZvSHYrR1RKWjF2MHM1cE9sRVZrMG9ZaXRBWllXVkhIOWJUQXgyR3hUOSsyaGpnQTJZOURRUXVvVlk3Y20KeGoxdUpVcnh2VTRMYjZycmZ2eEJmUkhaaGJZOFU4dzcyNDhhZmV2U1ljTk9KWVVTMmdKNnd5eXYvbndSUmhsUwplVVBMK1JzRVRrL3FRbHltUW5DWlRPQW5hTC9LTFhhZ3VVeXBhWTZXQWhTcytZR01aejRRUWpCOWFpbkNUSzZLCmRaZlFQOS9vUkNnSGdsVzlpYWxJaWhJanRyYVV5Uis4WHBLeWpVbENkWDhTNytUYy9uZEpMdFJYUUpSbkQ2UjIKS2NZeHVvMStDQ3dBOStoMXlZWDVMME1PUXhIdUVCcmNaZEFxVG1RTm1iQlFmSjJqZDFyYXVxdXE4WkovCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
+ client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBM1RQUEY5SWZJRWh1SHBJVm9YSnIxWThjcVV5SytBTmJ3ekZ0REtiQ2I2UC8zZXhOCnBkSnZla2NVQXM2UmNsZGFsamdidjlBOVJicXFheTBKMDlHaFNHc2V6Tk53a2RDdms4SEJPY2U1YW5mNitPUHcKZjZiQXJ3ZkhSOUlLblc0dk1BMlBkaXVrY2dQVWRzMVE2K29pWFVucExFZ3UvcmdXUCtnbU1BZU9OalJmdWxvcApTeHVBaU9FRTFTdTd3OVQ2eGZFNFZOc2ZaS291VnM2RWN6S29lN1FRMFM2Tm16Tko1bERQNWJRMi85TXBXa3lYClQ4SStONTZ4ZXlKVng3VWZiaXBoK1pMdnNORURWSytzRjFBWWhQekozeGIxakc0Q01ZTnNvYklBVk1jRS9xVUkKa01CelVKbjBFMU5aMXBuNEJUbnFjSXFjRUlNSVJJRjlmRll0VVFJREFRQUJBb0lCQUZuWGd4TlB1bWlvUW1HVQpQOGpVNmt0UTF2bEVKMlZZSjlyOXBpYnZUQ1YvM0pwTU1iVlo1UUVyQVV1cWpwUjhPa1N0QVVoRTBiNFNkTWtXCi85alNXY2xLQ0xaMFlsSTNDampmQnhYcXNybzFoTysxMUtaa0dmcDlGRWx0Vy9aWkhEMU9KS2lBVFVncG1nK3UKa0dGaE5SZml2eHZITFo4ek1ZWS9TZE5VSGtTUSsxRER6bGt5OGNCQ2pNV2ZqNmovcVg4OG5tQjU2MUJjc2ZRZApmdjh5bEgyenVTYjJiUlVoT1gxaTRSRzZXT21nR0ZjTnQ5eFJQejlMOGFGbmdCa1hxVENRTzBYaUFFdXlpTkp4CkxWSE1hMUZXeVMxMDRSa3JjSEdFNkp1ck5YMWVWeVhaeExHSWk0d01uVXUybHpVSXBuZ0FTZkc0ajlBdDN1aTUKdDlTa2pvRUNnWUVBLzBUSkl2SXVBdFJxMFFQbmtaY3BBUWdMeGFyOWVrdkpZRWV6STdmbGpUMEJMeVB1dldjZApmWmE5bXZLUGV6WitvdkFuY2FYWGxQbmwvOEExaEVhdUJlMmFYRjVGZ2xxK0IwREl4ZGlJamdpVTBneUJFM0lyCjZCRlQ3NXBVcDMyZWNlMHpIRWhNUnNkWE42K1B6cmdMcGJXQTI5SXViL1BWTU8rNU9IdmlQNGtDZ1lFQTNkWUoKKzlEc1R2MlNTNE53RmR0YUx1RTErRUtTQ0tMWitUSVArdUFHYTZBdUljTHpWWWEzV1FkcnJKL3dJM3V3QnRzSgpEaG16VG9EK2tRUjlERWZoNzBkcnNEaU9jS1hHeFN2ZWNEQ2s2RE9qZzlVa3FQbE5wWmNhWjg2UXJLUnEvUU5sCkF0QmxSbzQ0eDBZTDlHcDZiKzFkMDFYK0l1a1g4Mk96NU90QWhZa0NnWUVBdVpvbThMekx6WXpyR1l4UVBDQi8KVk51bnk1SjIrUXZQb0t5aFNOQTJITXFGYU9Ra2V4eFZhZkpIYkRqL01DUkFVWEp5QzRUOHlib2xqQjRTQzFwawp2N1N4N29VdzN3WGhjMTZjWEpZRE85cHJjb3BhODJ1cEZ2UTZabFY5UmNibGhJcG1CaU5mS250WUpBb0hjdE9JCnpIUzYrNW5IMytpWFV4eUtQTkorZjVrQ2dZQlM3aFZGWWxFMUptNXVkSENPZXpZM21GSEl3WG8rYUVMOGVjNm4KaHVCZms3NHJPT29tNnZuWCtvSXlRb2hQZTRuT3hrdVlUZHRPV2NMUkM5Q0Rxc3UzM0FkU0ZaS0tZaTd3dnZtbApEMHBXMUJjb3phR3EyYWhCWXorbjF2MFd6VkNCemFjUmFqNnlKYkRzTGkrQjY5eGh3MmJ6L28zSEYwU3c1SEQzCi8wcGJvUUtCZ0F5LzMzcGRxUWxOY3dVTDZaWlpWYzUwa0x1cC9rcnByZjBZcVd4M2lxQTVXNnpvSFVtSEZQVmgKU2U3T3RsaWFycllqQ3JQTVhWd0VwV1A4SnZSOWRoOEhoZWVEUi9ENk1FMnEwSjBYYmdqMXRoVjdpOXUxTDZOQwoxTXB3OTdoQ2NLcUVHc00xUmYzTmhuM21UVzJJdEc4cTFRZWF3OE1ScGpSVStaazVpdWhnCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
diff --git a/order-service/Dockerfile b/order-service/Dockerfile
index 1b8d8b6..8fa2b24 100644
--- a/order-service/Dockerfile
+++ b/order-service/Dockerfile
@@ -1,20 +1,42 @@
-# Use official Node.js image
-FROM node:22-alpine
+FROM node:22-alpine AS base
+
+FROM base AS development
-# Create app directory
WORKDIR /app
-# Copy package files
-COPY package*.json ./
+COPY package.json yarn.lock ./
+
+RUN yarn global add nodemon && yarn install --verbose
+
+COPY ./src .
+
+EXPOSE 3001
-# Install dependencies
-RUN npm install
+CMD ["yarn", "dev"]
+
+# Production image
+
+# Builder Stage
+FROM base AS builder
+
+WORKDIR /app
+
+COPY package.json ./
+
+# Install only production dependencies
+RUN yarn install --production --verbose && yarn cache clean
+
+COPY ./src .
+
+FROM node:22-alpine AS production
+
+LABEL org.opencontainers.image.title="orderservice"
+LABEL org.opencontainers.image.description="CraveDrop order Service"
+
+WORKDIR /app
-# Copy the rest of the code
-COPY . .
+COPY --from=builder /app .
-# Expose the port your app runs on
-EXPOSE 5000
+EXPOSE 3000
-# Start the app (ESM-compatible)
-CMD ["node", "index.js"]
+CMD [ "node","index.js" ]
\ No newline at end of file
diff --git a/order-service/docker-compose.yml b/order-service/docker-compose.yml
index 35ecf93..c45e6c5 100644
--- a/order-service/docker-compose.yml
+++ b/order-service/docker-compose.yml
@@ -2,7 +2,9 @@ version: '3.8'
services:
order-service:
- build: .
+ build:
+ context: .
+ target: development
ports:
- "5000:5000"
environment:
diff --git a/order-service/package.json b/order-service/package.json
index c1d3787..902d9a7 100644
--- a/order-service/package.json
+++ b/order-service/package.json
@@ -4,7 +4,6 @@
"main": "index.js",
"type": "module",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/index.js"
},
"keywords": [],
diff --git a/order-service/src/controller/order.controller.js b/order-service/src/controller/order.controller.js
index 187b7ab..edc011a 100644
--- a/order-service/src/controller/order.controller.js
+++ b/order-service/src/controller/order.controller.js
@@ -6,13 +6,14 @@ dotenv.config();
export const createOrder = async (req, res) => {
try {
- const { userId, items, paymentMethod, currency, deliveryAddress, phoneNumber } = req.body;
+ const { userId, items, paymentMethod, totalAmount, currency, deliveryAddress, phoneNumber } = req.body;
- if (!userId || !Array.isArray(items) || items.length === 0 || !paymentMethod || !currency || !deliveryAddress) {
+ if (!userId || !Array.isArray(items) || items.length === 0 || !paymentMethod || !deliveryAddress) {
return res.status(400).json({ message: 'Invalid request data' });
}
- let totalAmount = 0;
+ let paymentClientSecret = null;
+ let calculatedTotalAmount = 0;
const enrichedItems = [];
// Validate and enrich products
@@ -23,48 +24,78 @@ export const createOrder = async (req, res) => {
}
const itemTotal = product.price * item.quantity;
- totalAmount += itemTotal;
+ calculatedTotalAmount += itemTotal;
enrichedItems.push({
productId: product._id,
quantity: item.quantity,
priceAtPurchase: product.price,
+ restaurantId: product.restaurantId || 'default-restaurant' // Include restaurant ID from product
});
}
- let clientSecret = null;
-
- // If payment method is card, call Payment Service
+ // For card payments, we already processed the payment in the frontend
+ // so we don't need to call the payment service again
if (paymentMethod === 'card') {
- const paymentResponse = await axios.post(process.env.PAYMENT_SERVICE_URL, {
- amount: totalAmount,
- currency,
- });
- clientSecret = paymentResponse.data.clientSecret;
+ // If payment was already processed, we use the totalAmount from the request
+ // and we don't need to generate a new client secret
+ } else if (paymentMethod === 'cash') {
+ // For cash payments, no need to process anything here
}
- // Create the Order
+ // Create the Order - use the provided totalAmount for card payments (already processed)
+ // or use calculated amount for cash payments
+ const finalAmount = paymentMethod === 'card' ? totalAmount : calculatedTotalAmount;
+
const order = new Order({
userId,
items: enrichedItems,
- totalAmount,
+ totalAmount: finalAmount,
+ currency: currency || 'usd', // Use provided currency or default to 'usd'
paymentMethod,
- paymentClientSecret: clientSecret,
+ paymentClientSecret,
deliveryAddress,
phoneNumber,
+ status: paymentMethod === 'card' ? 'paid' : 'pending', // If card payment, it's already paid
});
- const savedOrder = await order.save();
+ await order.save();
- res.status(201).json(savedOrder);
+ //TODO call notification
+
+ res.status(201).json({
+ message: 'Order created successfully',
+ order,
+ paymentClientSecret,
+ });
} catch (error) {
console.error('Order creation failed:', error.message);
res.status(500).json({ message: 'Something went wrong on the server' });
}
};
+// Add a new endpoint to get orders by restaurant ID
+export const getOrdersByRestaurant = async (req, res) => {
+ try {
+ const { restaurantId } = req.params;
+
+ if (!restaurantId) {
+ return res.status(400).json({ message: 'Restaurant ID is required' });
+ }
+
+ // Find orders that contain items from the specified restaurant
+ const orders = await Order.find({
+ 'items.restaurantId': restaurantId
+ });
+ res.status(200).json(orders);
+ } catch (error) {
+ console.error('Get restaurant orders failed:', error);
+ res.status(500).json({ message: 'Something went wrong' });
+ }
+};
+// Rest of your controller functions remain the same
export const getOrderById = async (req, res) => {
try {
const order = await Order.findById(req.params.id);
@@ -103,7 +134,16 @@ export const deleteOrder = async (req, res) => {
export const getAllOrders = async (req, res) => {
try {
- const orders = await Order.find(); // Fetch all orders from the database
+ // Support filtering by restaurantId via query param
+ const { restaurantId } = req.query;
+
+ let query = {};
+ if (restaurantId) {
+ // Find orders with items from this restaurant
+ query = { 'items.restaurantId': restaurantId };
+ }
+
+ const orders = await Order.find(query);
res.status(200).json(orders);
} catch (error) {
console.error('Get all orders failed:', error);
diff --git a/order-service/src/controller/product.controller.js b/order-service/src/controller/product.controller.js
index c1f3e36..d06f1b1 100644
--- a/order-service/src/controller/product.controller.js
+++ b/order-service/src/controller/product.controller.js
@@ -5,13 +5,20 @@ dotenv.config();
export const createProduct = async (req, res) => {
try {
- const { name, description, price, image } = req.body;
+ const { name, description, price, image, restaurantId } = req.body;
if (!name || !price) {
return res.status(400).json({ message: 'Name and price are required' });
}
- const newProduct = new Product({ name, description, price, image });
+ const newProduct = new Product({
+ name,
+ description,
+ price,
+ image,
+ restaurantId: restaurantId || 'default-restaurant'
+ });
+
const savedProduct = await newProduct.save();
res.status(201).json(savedProduct);
@@ -33,9 +40,34 @@ export const getProductsById = async (req, res) => {
}
};
+// Get products by restaurant ID
+export const getProductsByRestaurant = async (req, res) => {
+ try {
+ const { restaurantId } = req.params;
+
+ if (!restaurantId) {
+ return res.status(400).json({ message: 'Restaurant ID is required' });
+ }
+
+ const products = await Product.find({ restaurantId });
+ res.status(200).json(products);
+ } catch (error) {
+ console.error('Get restaurant products failed:', error.message);
+ res.status(500).json({ message: 'Something went wrong' });
+ }
+};
+
export const getAllProducts = async (req, res) => {
try {
- const products = await Product.find();
+ // Support filtering by restaurantId via query param
+ const { restaurantId } = req.query;
+
+ let query = {};
+ if (restaurantId) {
+ query.restaurantId = restaurantId;
+ }
+
+ const products = await Product.find(query);
res.status(200).json(products);
} catch (error) {
console.error('Get all products failed:', error.message);
diff --git a/order-service/src/index.js b/order-service/src/index.js
index 990b992..a785592 100644
--- a/order-service/src/index.js
+++ b/order-service/src/index.js
@@ -2,6 +2,7 @@ import express from 'express';
import dotenv from 'dotenv';
import connectDB from './service/db.js';
import orderRoutes from './routes/order.route.js';
+import productsRoutes from './routes/product.route.js';
dotenv.config();
const app = express();
@@ -9,10 +10,25 @@ const PORT = process.env.PORT || 5000;
app.use(express.json());
+app.get('/api/orders/health', (req, res) => {
+ res.status(200).json({ status: 'ok' });
+});
+
// Versioned API route
-app.use('/api/v1/orders', orderRoutes);
+app.use('/api/orders', orderRoutes);
+app.use(`/api/products`, productsRoutes)
// Connect MongoDB and start server
-connectDB().then(() => {
- app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
-});
+const startServer = async () => {
+ try {
+ await connectDB();
+ app.listen(PORT, () => {
+ console.log(`Server running on port ${PORT}`);
+ });
+ } catch (error) {
+ console.error('Failed to connect to database:', error.message);
+ process.exit(1); // Exit the app with failure
+ }
+};
+
+startServer();
diff --git a/order-service/src/model/order.model.js b/order-service/src/model/order.model.js
index 9f8879c..60c37c8 100644
--- a/order-service/src/model/order.model.js
+++ b/order-service/src/model/order.model.js
@@ -3,7 +3,8 @@ import mongoose from 'mongoose';
const itemSchema = new mongoose.Schema({
productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true },
quantity: { type: Number, required: true },
- priceAtPurchase: { type: Number, required: true },
+ priceAtPurchase: { type: Number, required: true },
+ restaurantId: { type: String, required: true }, // Add restaurant ID to each item
});
const orderSchema = new mongoose.Schema(
@@ -11,6 +12,7 @@ const orderSchema = new mongoose.Schema(
userId: { type: String, required: true },
items: [itemSchema],
totalAmount: { type: Number, required: true },
+ currency: { type: String, default: 'usd' },
paymentMethod: { type: String, enum: ['card', 'cash'], required: true },
paymentClientSecret: { type: String },
status: { type: String, enum: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'], default: 'pending' },
diff --git a/order-service/src/model/product.model.js b/order-service/src/model/product.model.js
index 9007f4e..b3190ff 100644
--- a/order-service/src/model/product.model.js
+++ b/order-service/src/model/product.model.js
@@ -11,6 +11,11 @@ const productSchema = new mongoose.Schema({
type: Number,
required: true,
},
+ restaurantId: {
+ type: String,
+ required: true,
+ default: 'default-restaurant' // Default value for existing products
+ },
image: String,
}, { timestamps: true });
diff --git a/order-service/src/routes/order.route.js b/order-service/src/routes/order.route.js
index a0555e0..fa9c48e 100644
--- a/order-service/src/routes/order.route.js
+++ b/order-service/src/routes/order.route.js
@@ -4,15 +4,17 @@ import {
getOrderById,
updateOrder,
deleteOrder,
- getAllOrders
+ getAllOrders,
+ getOrdersByRestaurant
} from '../controller/order.controller.js';
const router = express.Router();
router.post('/', createOrder);
-router.get('/', getAllOrders); // Fetch all orders
router.get('/:id', getOrderById);
router.put('/:id', updateOrder);
router.delete('/:id', deleteOrder);
+router.get('/', getAllOrders); // Fetch all orders
+router.get('/restaurant/:restaurantId', getOrdersByRestaurant); // New endpoint for restaurant orders
export default router;
diff --git a/order-service/src/routes/product.route.js b/order-service/src/routes/product.route.js
index ee1e085..38c5190 100644
--- a/order-service/src/routes/product.route.js
+++ b/order-service/src/routes/product.route.js
@@ -1,12 +1,13 @@
// src/route/product.route.js
import express from 'express';
-import { createProduct, getAllProducts, getProductsById } from '../controller/product.controller.js';
+import { createProduct, getAllProducts, getProductsById, getProductsByRestaurant } from '../controller/product.controller.js';
const router = express.Router();
-router.post('/', createProduct);
-router.get('/:id', getProductsById);
-router.get('/', getAllProducts);
+router.post('/', createProduct);
+router.get('/:id', getProductsById);
+router.get('/', getAllProducts);
+router.get('/restaurant/:restaurantId', getProductsByRestaurant); // New endpoint for restaurant products
export default router;
diff --git a/payment-service/Dockerfile b/payment-service/Dockerfile
index f956e76..d27931d 100644
--- a/payment-service/Dockerfile
+++ b/payment-service/Dockerfile
@@ -1,13 +1,42 @@
-FROM node:22-alpine
+FROM node:22-alpine AS base
+
+FROM base AS development
WORKDIR /app
-COPY package*.json ./
+COPY package.json yarn.lock ./
+
+RUN yarn global add nodemon && yarn install --verbose
+
+COPY ./src .
+
+EXPOSE 3001
-RUN npm install
+CMD ["yarn", "dev"]
+
+# Production image
+
+# Builder Stage
+FROM base AS builder
+
+WORKDIR /app
+
+COPY package.json ./
+
+# Install only production dependencies
+RUN yarn install --production --verbose && yarn cache clean
+
+COPY ./src .
+
+FROM node:22-alpine AS production
+
+LABEL org.opencontainers.image.title="paymentservice"
+LABEL org.opencontainers.image.description="CraveDrop payment Service"
+
+WORKDIR /app
-COPY . .
+COPY --from=builder /app .
-EXPOSE 5002
+EXPOSE 3000
-CMD ["npm", "start"]
\ No newline at end of file
+CMD [ "node","index.js" ]
\ No newline at end of file
diff --git a/payment-service/package.json b/payment-service/package.json
index 50b8601..4033fa6 100644
--- a/payment-service/package.json
+++ b/payment-service/package.json
@@ -4,7 +4,6 @@
"main": "index.js",
"type": "module",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/index.js"
},
"keywords": [],
diff --git a/payment-service/src/config/stripe.js b/payment-service/src/config/stripe.js
new file mode 100644
index 0000000..3291643
--- /dev/null
+++ b/payment-service/src/config/stripe.js
@@ -0,0 +1,9 @@
+import Stripe from 'stripe';
+import dotenv from 'dotenv';
+dotenv.config();
+
+const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
+ apiVersion: '2023-10-16', // Always set API version
+});
+
+export default stripe;
diff --git a/payment-service/src/controller/payment.controllers.js b/payment-service/src/controller/payment.controllers.js
index 73c82fd..4efbc0e 100644
--- a/payment-service/src/controller/payment.controllers.js
+++ b/payment-service/src/controller/payment.controllers.js
@@ -1,8 +1,10 @@
import dotenv from 'dotenv';
dotenv.config();
import Stripe from 'stripe';
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
+// CREATE payment intent
export const createPaymentIntent = async (req, res) => {
try {
const { amount, currency } = req.body;
@@ -20,3 +22,30 @@ export const createPaymentIntent = async (req, res) => {
res.status(500).send({ error: error.message });
}
};
+
+// NEW: CONFIRM and SAVE payment
+export const confirmPayment = async (req, res) => {
+ try {
+ const { paymentIntentId } = req.body;
+
+ // Fetch the latest payment intent from Stripe
+ const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
+
+ if (paymentIntent.status !== 'succeeded') {
+ return res.status(400).json({ message: 'Payment not successful yet.' });
+ }
+
+ await Payment.create({
+ paymentIntentId: paymentIntent.id,
+ amount: paymentIntent.amount,
+ currency: paymentIntent.currency,
+ status: paymentIntent.status,
+ createdAt: new Date(),
+ });
+
+ res.status(200).json({ message: 'Payment confirmed and saved successfully.' });
+ } catch (error) {
+ console.error('Payment confirmation failed:', error);
+ res.status(500).send({ error: error.message });
+ }
+};
diff --git a/payment-service/src/index.js b/payment-service/src/index.js
index 1521c8e..43b03e3 100644
--- a/payment-service/src/index.js
+++ b/payment-service/src/index.js
@@ -6,12 +6,34 @@ import paymentRoutes from './routes/payment.route.js';
dotenv.config();
const app = express();
-app.use(cors());
app.use(express.json());
-app.use('/api/v1/payment', paymentRoutes);
+// CORS configuration with frontend URL from environment
+const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:5173';
-const PORT = process.env.PORT || 5002;
-app.listen(PORT, () => {
- console.log(`Payment service running on port ${PORT}`);
+app.use(cors({
+ origin: FRONTEND_URL,
+}));
+
+app.get('/api/payments/health', (req, res) => {
+ res.status(200).json({ status: 'ok' });
});
+
+// Payment routes
+app.use('/api/payments', paymentRoutes);
+
+// Start server with error handling
+const PORT = process.env.PORT || 5002;
+
+const startServer = async () => {
+ try {
+ app.listen(PORT, () => {
+ console.log(`Payment service running on port ${PORT}`);
+ });
+ } catch (error) {
+ console.error('Failed to start server:', error.message);
+ process.exit(1);
+ }
+};
+
+startServer();
diff --git a/payment-service/src/routes/payment.route.js b/payment-service/src/routes/payment.route.js
index d35f476..a5a914e 100644
--- a/payment-service/src/routes/payment.route.js
+++ b/payment-service/src/routes/payment.route.js
@@ -1,8 +1,9 @@
import express from 'express';
-import { createPaymentIntent } from '../controller/payment.controllers.js';
+import { createPaymentIntent, confirmPayment } from '../controller/payment.controllers.js';
const router = express.Router();
router.post('/create-payment-intent', createPaymentIntent);
+router.post('/confirm-payment', confirmPayment);
export default router;
\ No newline at end of file