├── src ├── redux │ ├── orderSlice.ts │ ├── userSlice.ts │ ├── reduxStore.ts │ ├── cartSlice.ts │ └── restaurantSlice.ts ├── vite-env.d.ts ├── lib │ └── utils.ts ├── hooks │ ├── useReduxTypeHooks.ts │ └── apiHooks │ │ ├── useGetCheckOut.ts │ │ ├── useGetCusines.ts │ │ ├── useGetAllRestaurant.ts │ │ ├── useGetRestaurant.ts │ │ ├── useGetRestOrders.ts │ │ └── useGetSingleRestaurant.ts ├── utils │ └── apiEndPoint.ts ├── schemaZOD │ ├── restaurantSchema.ts │ └── userSchem.ts ├── layouts │ ├── MainLayout.tsx │ └── DashboardLayout.tsx ├── pages │ ├── Home.tsx │ ├── RestaurantDetails.tsx │ ├── UserOrder.tsx │ ├── Admin │ │ ├── Dashboard.tsx │ │ ├── Menus.tsx │ │ └── Reastaurant.tsx │ ├── Cart.tsx │ ├── SearchPage.tsx │ └── Profile.tsx ├── components │ ├── ui │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ ├── dropdown-menu.tsx │ │ └── menubar.tsx │ └── shared │ │ ├── FilterOptions.tsx │ │ ├── FeatureSection.tsx │ │ ├── AvailableMenu.tsx │ │ ├── UserFeedback.tsx │ │ ├── RestaurantCard.tsx │ │ ├── PopularDishes.tsx │ │ ├── Admin │ │ ├── DashboardNav.tsx │ │ └── MenuDialog.tsx │ │ ├── HeroSection.tsx │ │ ├── Footer.tsx │ │ ├── CheckoutDialog.tsx │ │ └── Navbar.tsx ├── App.css 00-49-02-869.css ├── routes │ ├── PrivateRoute.tsx │ └── Routes.tsx ├── main.tsx ├── App.tsx ├── auth │ ├── ForgotPassword.tsx │ ├── ResetPassword.tsx │ ├── VerifyEmail.tsx │ ├── Login.tsx │ └── Register.tsx ├── assets 00-49-08-258 │ └── react.svg └── index.css ├── public └── .redirects ├── vercel.json ├── tsconfig.json ├── vite.config.ts ├── index.html ├── components.json ├── eslint.config.js ├── .gitignore ├── tsconfig.node.json ├── tsconfig.app.json ├── package.json └── README.md /src/redux/orderSlice.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/.redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "/(.*)", "destination": "/" } 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ], 11 | "compilerOptions": { 12 | "baseUrl": ".", 13 | "paths": { 14 | "@/*": ["./src/*"] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/hooks/useReduxTypeHooks.ts: -------------------------------------------------------------------------------- 1 | // hooks.ts 2 | import { useDispatch, useSelector, type TypedUseSelectorHook } from "react-redux"; 3 | import type { AppDispatch, RootState } from "../redux/reduxStore"; 4 | 5 | export const useAppDispatch = () => useDispatch(); 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /src/utils/apiEndPoint.ts: -------------------------------------------------------------------------------- 1 | export const USER_API_END_POINT = "https://mealmart-server-chi.vercel.app/api/users" 2 | export const RESTAURANT_API_END_POINT = "https://mealmart-server-chi.vercel.app/api/restaurants" 3 | export const MENU_API_END_POINT = "https://mealmart-server-chi.vercel.app/api/menus" 4 | export const ORDER_API_END_POINT = "https://mealmart-server-chi.vercel.app/api/orders" -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import tailwindcss from "@tailwindcss/vite" 3 | import react from "@vitejs/plugin-react" 4 | import { defineConfig } from "vite" 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(), tailwindcss()], 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "./src"), 12 | }, 13 | }, 14 | }) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MealMart 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/schemaZOD/restaurantSchema.ts: -------------------------------------------------------------------------------- 1 | 2 | import {z} from "zod" 3 | 4 | export const restaurantFormSchema = z.object({ 5 | restaurantName : z.string().nonempty({message : "Name is required"}) , 6 | city : z.string().nonempty({message: "City is required"}) , 7 | country: z.string().nonempty({message: "Country is required"}) , 8 | deliveryTime : z.any() , 9 | cuisines : z.array(z.string()) , 10 | }) 11 | 12 | export type RestaurantFormSchema = z.infer -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /src/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router" 2 | import Navbar from "../components/shared/Navbar" 3 | import Footer from "../components/shared/Footer" 4 | 5 | 6 | function MainLayout() { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | ) 18 | } 19 | 20 | export default MainLayout 21 | -------------------------------------------------------------------------------- /src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import FeatureSection from "../components/shared/FeatureSection" 2 | import HeroSection from "../components/shared/HeroSection" 3 | import PopularDishes from "../components/shared/PopularDishes" 4 | import UserFeedback from "../components/shared/UserFeedback" 5 | 6 | function Home() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 |
14 | ) 15 | } 16 | 17 | export default Home 18 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { globalIgnores } from 'eslint/config' 7 | 8 | export default tseslint.config([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs['recommended-latest'], 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | 4 | import { cn } from "../../lib/utils" 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | export { Label } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules/ 3 | 4 | # Build outputs 5 | dist/ 6 | build/ 7 | 8 | # Environment variables 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Logs 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | pnpm-debug.log* 20 | *.log 21 | 22 | # OS files 23 | .DS_Store 24 | Thumbs.db 25 | 26 | # Editor directories and files 27 | .vscode/ 28 | .idea/ 29 | *.swp 30 | 31 | # Coverage directory 32 | coverage/ 33 | 34 | # Temporary files 35 | *.tmp 36 | *.temp 37 | 38 | # TypeScript build 39 | *.tsbuildinfo 40 | 41 | # Next.js specific (যদি পরে Next.js ব্যবহার করো) 42 | .next/ 43 | 44 | # Cache 45 | .cache/ 46 | 47 | .vercel 48 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | // "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/apiHooks/useGetCheckOut.ts: -------------------------------------------------------------------------------- 1 | // import axios from "axios" 2 | // import { useEffect } from "react" 3 | // import { ORDER_API_END_POINT } from "../../utils/apiEndPoint" 4 | 5 | 6 | 7 | // const useGetCheckOut = ()=>{ 8 | // useEffect(()=>{ 9 | // const checkOut = async ()=>{ 10 | // try { 11 | // const res = axios.post(`${ORDER_API_END_POINT}/create-checkout-session` , {} , {withCredentials: true}) 12 | // if(res.data.success){ 13 | 14 | // } 15 | // } catch (error) { 16 | // console.log(error) 17 | // } 18 | // } 19 | // checkOut() 20 | // }, []) 21 | // } 22 | 23 | // export default useGetCheckOut -------------------------------------------------------------------------------- /src/App.css 00-49-02-869.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // "erasableSyntaxOnly": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /src/routes/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | // PrivateRoute.tsx 2 | import React,{ type ReactNode } from "react"; 3 | import { Navigate, Outlet } from "react-router"; 4 | import { useAppSelector } from "../hooks/useReduxTypeHooks"; 5 | import { toast } from "react-toastify"; 6 | 7 | interface Props { 8 | children?: ReactNode; 9 | redirectPath?: string; 10 | } 11 | 12 | const PrivateRoute: React.FC = ({ children, redirectPath = "/login" }) => { 13 | const { user } = useAppSelector((state) => state.user); 14 | 15 | if (!user) { 16 | return ; 17 | } 18 | 19 | if (!user?.admin) { 20 | toast("Unauthorized Access"); 21 | return ; 22 | } 23 | 24 | return <>{children ? children : }; 25 | }; 26 | 27 | export default PrivateRoute; 28 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import { RouterProvider } from 'react-router' 5 | import { ToastContainer } from "react-toastify"; 6 | import Router from './routes/Routes' 7 | import {Provider} from "react-redux" 8 | import { PersistGate } from 'redux-persist/integration/react' 9 | import {persistStore, type Persistor} from "redux-persist"; 10 | let persistor: Persistor = persistStore(store) 11 | import store from './redux/reduxStore' 12 | 13 | 14 | createRoot(document.getElementById('root')!).render( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | , 23 | ) 24 | -------------------------------------------------------------------------------- /src/hooks/apiHooks/useGetCusines.ts: -------------------------------------------------------------------------------- 1 | // import axios from "axios" 2 | // import { useEffect } from "react" 3 | // import { RESTAURANT_API_END_POINT } from "../../utils/apiEndPoint" 4 | // import { useAppDispatch } from "../useReduxTypeHooks" 5 | // import { setAllCuisines } from "../../redux/userSlice" 6 | 7 | // const useGetCuisines = ()=>{ 8 | // const dispatch = useAppDispatch() 9 | // useEffect(()=>{ 10 | // const fetchCuisines = async ()=>{ 11 | // try { 12 | // const res = await axios.get(`${RESTAURANT_API_END_POINT}/cuisines` , {withCredentials: true}) 13 | // if(res.data.success){ 14 | // dispatch(setAllCuisines(res.data.cuisines)) 15 | // } 16 | // } catch (error) { 17 | // console.log(error) 18 | // } 19 | // } 20 | // fetchCuisines() 21 | // },[dispatch ]) 22 | // } 23 | // export default useGetCuisines -------------------------------------------------------------------------------- /src/redux/userSlice.ts: -------------------------------------------------------------------------------- 1 | // frontend/src/redux/userSlice.ts 2 | import { createSlice, type PayloadAction, } from "@reduxjs/toolkit"; 3 | 4 | // User type for frontend 5 | export interface IUser { 6 | fullName: string; 7 | email: string; 8 | contact: string; 9 | address?: string; 10 | city?: string; 11 | country?: string; 12 | profilePicture?: string; 13 | admin?: boolean; 14 | } 15 | 16 | // Slice state type 17 | interface UserState { 18 | user: IUser | null; 19 | } 20 | 21 | const initialState: UserState = { 22 | user: null, 23 | }; 24 | 25 | const userSlice = createSlice({ 26 | name: "user", 27 | initialState, 28 | reducers: { 29 | setUser: (state, action: PayloadAction) => { 30 | state.user = action.payload; 31 | }, 32 | clearUser: (state) => { 33 | state.user = null; 34 | }, 35 | }, 36 | }); 37 | 38 | export const { setUser, clearUser } = userSlice.actions; 39 | export default userSlice.reducer; 40 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import reactLogo from './assets/react.svg' 3 | import viteLogo from '/vite.svg' 4 | import './App.css' 5 | 6 | function App() { 7 | const [count, setCount] = useState(0) 8 | 9 | return ( 10 | <> 11 | 19 |

Vite + React

20 |
21 | 24 |

25 | Edit src/App.tsx and save to test HMR 26 |

27 |
28 |

29 | Click on the Vite and React logos to learn more 30 |

31 | 32 | ) 33 | } 34 | 35 | export default App 36 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "../../lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /src/redux/reduxStore.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers ,configureStore} from "@reduxjs/toolkit" 2 | import userSlice from "./userSlice" 3 | import restaurantSlice from "./restaurantSlice" 4 | import cartSlice from "./cartSlice" 5 | 6 | import { 7 | persistReducer, 8 | FLUSH, 9 | REHYDRATE, 10 | PAUSE, 11 | PERSIST, 12 | PURGE, 13 | REGISTER, 14 | } from 'redux-persist' 15 | import storage from 'redux-persist/lib/storage' 16 | 17 | 18 | const persistConfig = { 19 | key: 'root', 20 | version: 1, 21 | storage, 22 | } 23 | 24 | const rootReducer = combineReducers({ 25 | user : userSlice , 26 | restaurant: restaurantSlice, 27 | cart: cartSlice, 28 | 29 | }) 30 | const persistedReducer = persistReducer(persistConfig, rootReducer) 31 | const store = configureStore({ 32 | reducer: persistedReducer, 33 | middleware: (getDefaultMiddleware) => 34 | getDefaultMiddleware({ 35 | serializableCheck: { 36 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], 37 | }, 38 | }), 39 | }) 40 | 41 | export default store 42 | 43 | export type RootState = ReturnType 44 | export type AppDispatch = typeof store.dispatch -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "../../lib/utils" 5 | function Avatar({ 6 | className, 7 | ...props 8 | }: React.ComponentProps) { 9 | return ( 10 | 18 | ) 19 | } 20 | 21 | function AvatarImage({ 22 | className, 23 | ...props 24 | }: React.ComponentProps) { 25 | return ( 26 | 31 | ) 32 | } 33 | 34 | function AvatarFallback({ 35 | className, 36 | ...props 37 | }: React.ComponentProps) { 38 | return ( 39 | 47 | ) 48 | } 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/hooks/apiHooks/useGetAllRestaurant.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { useEffect } from "react" 3 | import { RESTAURANT_API_END_POINT } from "../../utils/apiEndPoint" 4 | import { setAllRestaurant, setPagination } from "../../redux/restaurantSlice" 5 | import { useAppDispatch, useAppSelector } from "../useReduxTypeHooks" 6 | 7 | const useGetAllRestaurant = ({ searchText = "", cuisines = "", dependency = "" }) => { 8 | const { pagination } = useAppSelector((state) => state.restaurant) 9 | const limit = pagination?.limit || 10; 10 | const page = pagination?.page || 1; 11 | 12 | const dispatch = useAppDispatch() 13 | 14 | useEffect(() => { 15 | const fetchAllRestaurant = async () => { 16 | try { 17 | const res = await axios.get( 18 | `${RESTAURANT_API_END_POINT}/search?searchText=${searchText}&cuisines=${cuisines}&limit=${limit}&page=${page}`, 19 | { withCredentials: true } 20 | ) 21 | if (res.data.success) { 22 | dispatch(setAllRestaurant(res.data.restaurants)) 23 | dispatch(setPagination(res.data.pagination)) 24 | } 25 | } catch (error) { 26 | console.log(error) 27 | } 28 | } 29 | fetchAllRestaurant() 30 | }, [dispatch, dependency, limit, page, searchText, cuisines]) 31 | } 32 | 33 | export default useGetAllRestaurant 34 | -------------------------------------------------------------------------------- /src/components/shared/FilterOptions.tsx: -------------------------------------------------------------------------------- 1 | // FilterOptions.tsx 2 | import React from "react"; 3 | 4 | interface FilterOptionsProps { 5 | selectedFilters: string[]; 6 | setSelectedFilters: (filters: string[]) => void; 7 | } 8 | 9 | const cuisines = ["Pizza", "Cakes" , "Noodles" , "Burgers" , "Cafe" , "Pasta" , "Tehari" , "Kebaba" , "Biryani" ,"Chicken" , "Pulao" , "Kacchi"]; 10 | 11 | const FilterOptions: React.FC = ({ selectedFilters, setSelectedFilters }) => { 12 | 13 | const handleFilterClick = (cuisine: string) => { 14 | if (selectedFilters.includes(cuisine)) { 15 | setSelectedFilters(selectedFilters.filter(f => f !== cuisine)); 16 | } else { 17 | setSelectedFilters([...selectedFilters, cuisine]); 18 | } 19 | }; 20 | 21 | return ( 22 |
23 | {cuisines.map((cuisine) => ( 24 | 35 | ))} 36 |
37 | ); 38 | }; 39 | 40 | export default FilterOptions; 41 | -------------------------------------------------------------------------------- /src/hooks/apiHooks/useGetRestaurant.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import axios from "axios"; 3 | import { RESTAURANT_API_END_POINT } from "../../utils/apiEndPoint"; 4 | import { useAppDispatch, useAppSelector } from "../useReduxTypeHooks"; 5 | import { setRestaurant } from "../../redux/restaurantSlice"; 6 | 7 | const useGetRestaurant = ({ dependency }: { dependency?: unknown }) => { 8 | const {user} = useAppSelector((state)=>state.user) 9 | const dispatch = useAppDispatch(); 10 | const [loading, setLoading] = useState(true); 11 | const [error, setError] = useState(null); 12 | 13 | useEffect(() => { 14 | const fetchRestaurant = async () => { 15 | 16 | try { 17 | dispatch(setRestaurant(null)) 18 | setLoading(true); 19 | const res = await axios.get( 20 | `${RESTAURANT_API_END_POINT}/own`, 21 | { withCredentials: true } 22 | ); 23 | 24 | if (res.data.success) { 25 | dispatch(setRestaurant(res.data.restaurant)); 26 | } 27 | } catch (err: any) { 28 | setError(err.response?.data?.message || "Something went wrong"); 29 | } finally { 30 | setLoading(false); 31 | } 32 | }; 33 | 34 | fetchRestaurant(); 35 | }, [dispatch , dependency , user]); 36 | 37 | return { loading, error }; 38 | }; 39 | 40 | export default useGetRestaurant; 41 | -------------------------------------------------------------------------------- /src/hooks/apiHooks/useGetRestOrders.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useEffect, useState } from "react"; 3 | import { RESTAURANT_API_END_POINT } from "../../utils/apiEndPoint"; 4 | import { useAppDispatch } from "../useReduxTypeHooks"; 5 | import { setOrders } from "../../redux/restaurantSlice"; 6 | 7 | const useGetRestOrders = ({ dependency }: { dependency?: unknown }) => { 8 | const dispatch = useAppDispatch(); 9 | const [loading, setLoading] = useState(false); 10 | const [error, setError] = useState(null); 11 | 12 | useEffect(() => { 13 | const fetchOrder = async () => { 14 | setLoading(true); 15 | setError(null); 16 | 17 | try { 18 | const res = await axios.get(`${RESTAURANT_API_END_POINT}/orders`, { 19 | withCredentials: true, 20 | }); 21 | 22 | if (res.data.success) { 23 | dispatch(setOrders(res.data.orders || [])); 24 | } else { 25 | dispatch(setOrders([])) 26 | setError(res.data.message || "Something went wrong"); 27 | } 28 | } catch (err: any) { 29 | console.log(err); 30 | setError(err.message || "Network error"); 31 | } finally { 32 | setLoading(false); 33 | } 34 | }; 35 | 36 | fetchOrder(); 37 | }, [dispatch, dependency]); 38 | 39 | return { loading, error }; 40 | }; 41 | 42 | export default useGetRestOrders; 43 | -------------------------------------------------------------------------------- /src/hooks/apiHooks/useGetSingleRestaurant.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import axios from "axios"; 3 | import { RESTAURANT_API_END_POINT } from "../../utils/apiEndPoint"; 4 | import { useAppDispatch} from "../useReduxTypeHooks"; 5 | import { setSingleRestaurant } from "../../redux/restaurantSlice"; 6 | 7 | const useGetSingleRestaurant = ({restaurantId = "", dependency = null}) => { 8 | 9 | const dispatch = useAppDispatch(); 10 | const [loading, setLoading] = useState(true); 11 | const [error, setError] = useState(null); 12 | 13 | useEffect(() => { 14 | if (!restaurantId) return; 15 | const fetchRestaurant = async () => { 16 | try { 17 | setLoading(true); 18 | dispatch(setSingleRestaurant(null)) 19 | const res = await axios.get( 20 | `${RESTAURANT_API_END_POINT}/${restaurantId}`, 21 | { withCredentials: true } 22 | ); 23 | 24 | if (res.data.success) { 25 | dispatch(setSingleRestaurant(res.data.restaurant)); 26 | } 27 | } catch (err: any) { 28 | setError(err.response?.data?.message || "Something went wrong"); 29 | } finally { 30 | setLoading(false); 31 | } 32 | }; 33 | 34 | fetchRestaurant(); 35 | }, [dispatch , dependency ,restaurantId ]); 36 | 37 | return { loading, error }; 38 | }; 39 | 40 | export default useGetSingleRestaurant; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mealmart", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-avatar": "^1.1.10", 14 | "@radix-ui/react-dialog": "^1.1.15", 15 | "@radix-ui/react-dropdown-menu": "^2.1.15", 16 | "@radix-ui/react-label": "^2.1.7", 17 | "@radix-ui/react-menubar": "^1.1.15", 18 | "@radix-ui/react-slot": "^1.2.3", 19 | "@reduxjs/toolkit": "^2.8.2", 20 | "@tailwindcss/vite": "^4.1.11", 21 | "axios": "^1.11.0", 22 | "class-variance-authority": "^0.7.1", 23 | "clsx": "^2.1.1", 24 | "framer-motion": "^12.23.12", 25 | "lucide-react": "^0.536.0", 26 | "react": "^19.1.0", 27 | "react-dom": "^19.1.0", 28 | "react-icons": "^5.5.0", 29 | "react-redux": "^9.2.0", 30 | "react-router": "^7.7.1", 31 | "react-toastify": "^11.0.5", 32 | "redux-persist": "^6.0.0", 33 | "tailwind-merge": "^3.3.1", 34 | "tailwindcss": "^4.1.11", 35 | "zod": "^4.0.14" 36 | }, 37 | "devDependencies": { 38 | "@eslint/js": "^9.30.1", 39 | "@types/node": "^24.5.2", 40 | "@types/react": "^19.1.8", 41 | "@types/react-dom": "^19.1.6", 42 | "@vitejs/plugin-react": "^4.6.0", 43 | "eslint": "^9.30.1", 44 | "eslint-plugin-react-hooks": "^5.2.0", 45 | "eslint-plugin-react-refresh": "^0.4.20", 46 | "globals": "^16.3.0", 47 | "tw-animate-css": "^1.3.6", 48 | "typescript": "~5.8.3", 49 | "typescript-eslint": "^8.35.1", 50 | "vite": "^7.0.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/shared/FeatureSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FaLeaf, FaTruck, FaStar } from "react-icons/fa"; 3 | import { motion } from "framer-motion"; 4 | 5 | const FeatureSection: React.FC = () => { 6 | const features = [ 7 | { icon: , title: "Fresh Ingredients", desc: "We use only the freshest ingredients in every meal." }, 8 | { icon: , title: "Fast Delivery", desc: "Get your food delivered hot & fresh to your doorstep." }, 9 | { icon: , title: "Top Quality", desc: "Our dishes are prepared with high-quality standards." }, 10 | ]; 11 | 12 | return ( 13 |
14 |
15 |

Why Choose MealMart?

16 |
17 | {features.map((feature, idx) => ( 18 | 26 |
{feature.icon}
27 |

{feature.title}

28 |

{feature.desc}

29 |
30 | ))} 31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default FeatureSection; 38 | -------------------------------------------------------------------------------- /src/layouts/DashboardLayout.tsx: -------------------------------------------------------------------------------- 1 | // DashboardLayout.tsx 2 | import { useState, useEffect } from 'react'; 3 | import { Outlet } from 'react-router'; 4 | import DashboardNav from '../components/shared/Admin/DashboardNav'; 5 | import Footer from '../components/shared/Footer'; 6 | import { IoMdClose, IoMdMenu } from 'react-icons/io'; 7 | 8 | const DashboardLayout = () => { 9 | const [isMobileOpen, setIsMobileOpen] = useState(false); 10 | 11 | // Mobile overlay close when clicking outside 12 | useEffect(() => { 13 | const handleClickOutside = (e: MouseEvent) => { 14 | const target = e.target as HTMLElement; 15 | if (isMobileOpen && !target.closest('#sidebar') && !target.closest('#mobile-menu-btn')) { 16 | setIsMobileOpen(false); 17 | } 18 | }; 19 | document.addEventListener('click', handleClickOutside); 20 | return () => document.removeEventListener('click', handleClickOutside); 21 | }, [isMobileOpen]); 22 | 23 | return ( 24 |
25 | {/* Sidebar */} 26 | 27 | 28 | {/* Main Content */} 29 |
30 | {/* Mobile toggle button */} 31 | 38 | 39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default DashboardLayout; 50 | -------------------------------------------------------------------------------- /src/components/shared/AvailableMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useAppDispatch } from "../../hooks/useReduxTypeHooks"; 2 | import { addToCart } from "../../redux/cartSlice"; 3 | import { Button } from "../ui/button"; 4 | 5 | interface MenuItem { 6 | foodName: string; 7 | description: string; 8 | price: number; 9 | foodImage: string; 10 | _id: string; 11 | } 12 | 13 | interface AvailableMenuProps { 14 | menus: MenuItem[]; 15 | restaurantId: string; 16 | } 17 | 18 | function AvailableMenu({ menus, restaurantId }: AvailableMenuProps) { 19 | const dispatch = useAppDispatch(); 20 | 21 | return ( 22 |
23 | {menus?.map((menu: MenuItem, index: number) => ( 24 |
25 |
26 |
27 | {menu.foodName} 32 |
33 |
34 |

{menu.foodName}

35 |

{menu.description}

36 |

37 | Price: ${menu.price} 38 |

39 | 40 | 46 |
47 |
48 |
49 | ))} 50 |
51 | ); 52 | } 53 | 54 | export default AvailableMenu; 55 | -------------------------------------------------------------------------------- /src/redux/cartSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; 2 | 3 | // Cart Item Type Define 4 | export interface CartItem { 5 | _id: string; 6 | foodName: string; 7 | price: number; 8 | image?: string; 9 | foodImage?: string; 10 | quantity: number; 11 | restaurantId: string; 12 | } 13 | 14 | // Slice Initial State 15 | interface CartState { 16 | foods: CartItem[]; 17 | } 18 | 19 | const initialState: CartState = { 20 | foods: [], 21 | }; 22 | 23 | const cartSlice = createSlice({ 24 | name: "cart", 25 | initialState, 26 | reducers: { 27 | // Add to Cart 28 | addToCart: ( 29 | state, 30 | action: PayloadAction & { restaurantId: string }> 31 | ) => { 32 | if (!state.foods) state.foods = []; 33 | 34 | const existingItem = state.foods.find((item) => item._id === action.payload._id); 35 | 36 | if (existingItem) { 37 | existingItem.quantity += 1; 38 | } else { 39 | state.foods.push({ ...action.payload, quantity: 1 }); 40 | } 41 | }, 42 | 43 | // Increase Quantity 44 | increaseQuantity: (state, action: PayloadAction) => { 45 | const item = state.foods.find((p) => p._id === action.payload); 46 | if (item) { 47 | item.quantity += 1; 48 | } 49 | }, 50 | 51 | // Decrease Quantity 52 | decreaseQuantity: (state, action: PayloadAction) => { 53 | const item = state.foods.find((p) => p._id === action.payload); 54 | if (!item) return; 55 | 56 | if (item.quantity > 1) { 57 | item.quantity -= 1; 58 | } else { 59 | state.foods = state.foods.filter((p) => p._id !== action.payload); 60 | } 61 | }, 62 | 63 | // Delete From Cart 64 | deleteFromCart: (state, action: PayloadAction) => { 65 | state.foods = state.foods.filter((p) => p._id !== action.payload); 66 | }, 67 | 68 | // Clear entire cart 69 | clearCart: (state) => { 70 | state.foods = []; 71 | }, 72 | }, 73 | }); 74 | 75 | export const { 76 | addToCart, 77 | increaseQuantity, 78 | decreaseQuantity, 79 | deleteFromCart, 80 | clearCart, 81 | } = cartSlice.actions; 82 | 83 | export default cartSlice.reducer; 84 | -------------------------------------------------------------------------------- /src/components/shared/UserFeedback.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion } from "framer-motion"; 3 | 4 | const UserFeedback: React.FC = () => { 5 | const feedbacks = [ 6 | { 7 | name: "Sara Williams", 8 | feedback: "Amazing food and super fast delivery! MealMart has become my go-to for dinner.", 9 | img: "https://randomuser.me/api/portraits/women/44.jpg", 10 | }, 11 | { 12 | name: "James Smith", 13 | feedback: "Great variety of meals. The ordering process is seamless and easy to use.", 14 | img: "https://randomuser.me/api/portraits/men/46.jpg", 15 | }, 16 | { 17 | name: "Emily Johnson", 18 | feedback: "Highly recommend MealMart! Delicious meals and very responsive customer service.", 19 | img: "https://randomuser.me/api/portraits/women/68.jpg", 20 | }, 21 | ]; 22 | 23 | return ( 24 |
25 |
26 |

What Our Users Say

27 | 28 |
29 | {feedbacks.map((user, idx) => ( 30 | 38 | {user.name} (e.currentTarget.src = "https://via.placeholder.com/150")} 43 | /> 44 |

"{user.feedback}"

45 |

{user.name}

46 |
47 | ))} 48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default UserFeedback; 55 | -------------------------------------------------------------------------------- /src/redux/restaurantSlice.ts: -------------------------------------------------------------------------------- 1 | // frontend/src/redux/restaurantSlice.ts 2 | import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; 3 | 4 | // Frontend types (mongoose type not needed here) 5 | export interface IFrontendRestaurant { 6 | _id: string; 7 | owner?: string; 8 | restaurantName: string; 9 | city: string; 10 | country: string; 11 | deliveryTime: number; 12 | cuisines: string[]; 13 | menus?: any[]; 14 | coverImage: string; 15 | 16 | } 17 | 18 | interface Pagination { 19 | total: number; 20 | page: number; 21 | limit: number; 22 | totalPages: number; 23 | } 24 | 25 | interface RestaurantState { 26 | restaurant: IFrontendRestaurant | null; 27 | singleRestaurant: IFrontendRestaurant | null; 28 | menu: any[]; 29 | allRestaurant: IFrontendRestaurant[]; 30 | pagination: Pagination; 31 | orders: any[]; 32 | } 33 | 34 | const initialState: RestaurantState = { 35 | restaurant: null, 36 | singleRestaurant: null, 37 | menu: [], 38 | allRestaurant: [], 39 | pagination: { 40 | total: 0, 41 | page: 1, 42 | limit: 10, 43 | totalPages: 0, 44 | }, 45 | orders: [], 46 | }; 47 | 48 | const restaurantSlice = createSlice({ 49 | name: "restaurant", 50 | initialState, 51 | reducers: { 52 | setRestaurant: (state, action: PayloadAction) => { 53 | state.restaurant = action.payload; 54 | }, 55 | setMenu: (state, action: PayloadAction) => { 56 | state.menu = action.payload; 57 | }, 58 | setAllRestaurant: (state, action: PayloadAction) => { 59 | state.allRestaurant = action.payload; 60 | }, 61 | setPagination: (state, action: PayloadAction) => { 62 | state.pagination = action.payload; 63 | }, 64 | setOrders: (state, action: PayloadAction) => { 65 | state.orders = action.payload; 66 | }, 67 | setSingleRestaurant: (state, action: PayloadAction) => { 68 | state.singleRestaurant = action.payload; 69 | }, 70 | }, 71 | }); 72 | 73 | export const { 74 | setRestaurant, 75 | setMenu, 76 | setAllRestaurant, 77 | setPagination, 78 | setOrders, 79 | setSingleRestaurant, 80 | } = restaurantSlice.actions; 81 | 82 | export default restaurantSlice.reducer; 83 | -------------------------------------------------------------------------------- /src/auth/ForgotPassword.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { MdOutlineEmail } from 'react-icons/md' 3 | import { Input } from '../components/ui/input' 4 | import { Label } from '@radix-ui/react-label' 5 | import { Button } from '../components/ui/button' 6 | import { Loader2 } from 'lucide-react' 7 | import { Link } from 'react-router' 8 | 9 | function ForgotPassword() { 10 | const [email , setEmail] = useState("") 11 | const [isLoading , setIsLoading] = useState(false) 12 | 13 | const handleSub = ()=>{ 14 | setIsLoading(false) 15 | } 16 | return ( 17 |
18 |
19 |
20 |

Forgot Password

21 |

Enter your email adress to reset your password

22 |
23 |
24 |
25 | 26 |
27 | )=>{setEmail(e.target.value)}} 32 | type='email' 33 | placeholder='Enter your email' 34 | /> 35 | 36 |
37 | { 38 | isLoading ? : 43 | } 44 | 45 |

Back to Login

46 |
47 |
48 | ) 49 | } 50 | 51 | export default ForgotPassword 52 | -------------------------------------------------------------------------------- /src/schemaZOD/userSchem.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const userRegisterSchema = z.object({ 4 | fullName: z.string().min(1, "Full name is required"), 5 | email: z.string().email("Invalid email address"), 6 | password: z 7 | .string() 8 | .superRefine((val, ctx) => { 9 | if (val.length < 6) { 10 | ctx.addIssue({ 11 | code: z.ZodIssueCode.custom, 12 | message: "Password must be at least 6 characters", 13 | }); 14 | return; 15 | } 16 | if (!/\d/.test(val)) { 17 | ctx.addIssue({ 18 | code: z.ZodIssueCode.custom, 19 | message: "Password must contain at least one number", 20 | }); 21 | } 22 | if (!/[A-Za-z]/.test(val)) { 23 | ctx.addIssue({ 24 | code: z.ZodIssueCode.custom, 25 | message: "Password must contain at least one letter", 26 | }); 27 | } 28 | }), 29 | contact: z 30 | .string() 31 | .min(10 , "Contact must be at least 10 digit"), 32 | }); 33 | 34 | export type registerInputState = z.infer; 35 | 36 | 37 | export const userLoginSchema = z.object({ 38 | email: z.string().email("Invalid email address"), 39 | password: z 40 | .string() 41 | .superRefine((val, ctx) => { 42 | if (val.length < 6) { 43 | ctx.addIssue({ 44 | code: z.ZodIssueCode.custom, 45 | message: "Password must be at least 6 characters", 46 | }); 47 | return; 48 | } 49 | if (!/\d/.test(val)) { 50 | ctx.addIssue({ 51 | code: z.ZodIssueCode.custom, 52 | message: "Password must contain at least one number", 53 | }); 54 | } 55 | if (!/[A-Za-z]/.test(val)) { 56 | ctx.addIssue({ 57 | code: z.ZodIssueCode.custom, 58 | message: "Password must contain at least one letter", 59 | }); 60 | } 61 | }), 62 | }); 63 | 64 | export type loginInputState = z.infer; 65 | -------------------------------------------------------------------------------- /src/components/shared/RestaurantCard.tsx: -------------------------------------------------------------------------------- 1 | import { FiFlag, FiMapPin } from "react-icons/fi"; 2 | import { useNavigate } from "react-router"; 3 | 4 | interface Restaurant { 5 | _id: string; 6 | restaurantName: string; 7 | coverImage: string; 8 | city: string; 9 | country: string; 10 | cuisines: string[]; 11 | } 12 | 13 | const RestaurantCard: React.FC<{ rest: Restaurant }> = ({ rest }) => { 14 | const navigate = useNavigate(); 15 | 16 | return ( 17 |
20 | {/* Image */} 21 | {rest.restaurantName} 26 | 27 | {/* Content */} 28 |
29 |

{rest?.restaurantName}

30 | 31 | {/* City */} 32 |
33 | 34 | City: 35 | {rest?.city} 36 |
37 | 38 | {/* Country */} 39 |
40 | 41 | Country: 42 | {rest?.country} 43 |
44 | 45 | {/* Cuisines */} 46 |
47 | {rest.cuisines.map((cuisine, index) => ( 48 | 52 | {cuisine} 53 | 54 | ))} 55 |
56 | 57 | {/* Button */} 58 |
59 | 65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | export default RestaurantCard; 72 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "../../lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /src/components/shared/PopularDishes.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion } from "framer-motion"; 3 | import { Link } from "react-router"; 4 | 5 | const PopularDishes: React.FC = () => { 6 | const dishes = [ 7 | { name: "Biryani Special", price: 15, img: "https://images.unsplash.com/photo-1600891964599-f61ba0e24092?auto=format&fit=crop&w=800&q=80" , path: "/search/sultan%20dine" } , 8 | { name: "Smash Burger", price: 12, img: "https://images.unsplash.com/photo-1550547660-d9450f859349?auto=format&fit=crop&w=800&q=80" , path: "/search/Pizzaburg" }, 9 | { name: "Vegan Salad", price: 10, img: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSL1-WBv2e8SFSBMSyX69QRtMDchFQNTdsRNA&s" , path: "/search/Captain" }, 10 | ]; 11 | 12 | return ( 13 |
14 |
15 |

Our Bestsellings

16 |
17 | {dishes.map((dish, idx) => ( 18 | 19 | 27 |
28 | {dish.name} (e.currentTarget.src = "https://via.placeholder.com/400x300?text=Image+Not+Found")} 33 | /> 34 |
35 |
36 |

{dish.name}

37 |

${dish.price}

38 |
39 |
40 | 41 | ))} 42 |
43 |
44 |
45 | ); 46 | }; 47 | 48 | export default PopularDishes; 49 | -------------------------------------------------------------------------------- /src/pages/RestaurantDetails.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { FiClock } from 'react-icons/fi'; 3 | import AvailableMenu from '../components/shared/AvailableMenu'; 4 | import { useParams } from 'react-router'; 5 | import useGetSingleRestaurant from '../hooks/apiHooks/useGetSingleRestaurant'; 6 | import { useAppSelector } from '../hooks/useReduxTypeHooks'; 7 | 8 | 9 | 10 | 11 | const RestaurantDetails: React.FC = () => { 12 | useEffect(() => { 13 | document.title = `Restaurant | MealMart`; 14 | }, []); 15 | 16 | const { restaurantId } = useParams(); 17 | const { singleRestaurant } = useAppSelector((state)=> state.restaurant ); 18 | 19 | useGetSingleRestaurant({ restaurantId }); 20 | 21 | return ( 22 |
23 |
24 |
25 | {singleRestaurant?.restaurantName 30 |

31 | {singleRestaurant?.restaurantName} 32 |

33 | 34 |
35 | {singleRestaurant?.cuisines && ( 36 |
37 | {singleRestaurant.cuisines.map((cuisine: string, index: number) => ( 38 | 42 | {cuisine} 43 | 44 | ))} 45 |
46 | )} 47 | 48 |

49 | Delivery time:{' '} 50 | 51 | {singleRestaurant?.deliveryTime} 52 | 53 |

54 | 55 |

56 | Available Menus: {singleRestaurant?.menus?.length || 0} 57 |

58 |
59 | 60 | {singleRestaurant?.menus && restaurantId && ( 61 | 65 | )} 66 |
67 |
68 |
69 | ); 70 | }; 71 | 72 | export default RestaurantDetails; 73 | -------------------------------------------------------------------------------- /src/components/shared/Admin/DashboardNav.tsx: -------------------------------------------------------------------------------- 1 | // DashboardNav.tsx 2 | import React from 'react'; 3 | import { Link, useLocation } from 'react-router'; 4 | import { FaHome } from "react-icons/fa"; 5 | import { RxDashboard } from "react-icons/rx"; 6 | import { MdOutlineRestaurant } from "react-icons/md"; 7 | import { IoIosAddCircleOutline } from "react-icons/io"; 8 | 9 | interface Props { 10 | isMobileOpen: boolean; 11 | setIsMobileOpen: React.Dispatch>; 12 | } 13 | 14 | const DashboardNav: React.FC = ({ isMobileOpen, setIsMobileOpen }) => { 15 | const location = useLocation(); 16 | 17 | const navItems = [ 18 | { name: "Home", path: "/", icon: }, 19 | { name: "Orders", path: "/dashboard", icon: }, 20 | { name: "Restaurant", path: "/dashboard/restaurant", icon: }, 21 | { name: "Menu", path: "/dashboard/add-menu", icon: }, 22 | ]; 23 | 24 | return ( 25 | <> 26 | {/* Desktop Sidebar */} 27 | 60 | 61 | {/* Mobile overlay */} 62 | {isMobileOpen && ( 63 |
64 | )} 65 | 66 | ); 67 | }; 68 | 69 | export default DashboardNav; 70 | -------------------------------------------------------------------------------- /src/auth/ResetPassword.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { MdLockOutline } from 'react-icons/md' 3 | import { Input } from '../components/ui/input' 4 | import { Label } from '@radix-ui/react-label' 5 | import { Button } from '../components/ui/button' 6 | import { Loader2 } from 'lucide-react' 7 | import { Link } from 'react-router' 8 | import { IoMdEye, IoMdEyeOff } from 'react-icons/io' 9 | 10 | function ResetPassword() { 11 | const [newPassword , setNewPassword] = useState("") 12 | const [isLoading , setIsLoading] = useState(false) 13 | const [isVisible , setIsVisible] = useState(false) 14 | 15 | const handleSub = ()=>{ 16 | setIsLoading(false) 17 | } 18 | 19 | return ( 20 |
21 |
22 |
23 |

Reset Password

24 |

Enter your new password to reset your old password

25 |
26 |
27 |
28 |
29 | )=>{setNewPassword(e.target.value)}} 34 | type={isVisible? "text" : "password"} 35 | placeholder='Enter your new password' 36 | /> 37 | 38 | {isVisible?setIsVisible(!isVisible)} size={20} className=' absolute right-0 inset-y-9 mr-2 cursor-pointer' />:setIsVisible(!isVisible)} size={20} className=' absolute right-0 inset-y-9 mr-2 cursor-pointer' /> } 39 | 40 |
41 | { 42 | isLoading ? : 47 | } 48 | 49 |

Back to Login

50 |
51 |
52 | ) 53 | } 54 | 55 | export default ResetPassword 56 | -------------------------------------------------------------------------------- /src/components/shared/HeroSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useNavigate } from "react-router"; 3 | import { FiSearch } from "react-icons/fi"; 4 | 5 | 6 | const HeroSection: React.FC = () => { 7 | const [searchText, setSearchText] = useState(""); 8 | const navigate = useNavigate(); 9 | 10 | const handleSearch = () => { 11 | if (searchText.trim()) { 12 | navigate(`/search/${encodeURIComponent(searchText.trim())}`); 13 | } 14 | }; 15 | 16 | const handleKeyPress = (e: React.KeyboardEvent) => { 17 | if (e.key === "Enter") handleSearch(); 18 | }; 19 | 20 | return ( 21 |
22 |
23 | 24 | {/* Left Text + Search */} 25 |
26 |

27 | Discover Delicious Meals at MealMart 28 |

29 |

30 | Search restaurants by name, city, or country and explore your next favorite meal. 31 |

32 | 33 | {/* Search Bar */} 34 |
35 | setSearchText(e.target.value)} 40 | onKeyDown={handleKeyPress} 41 | className="flex-1 px-4 py-3 outline-none text-gray-700 " 42 | /> 43 | 49 |
50 |
51 | 52 | {/* Right Image */} 53 |
54 | Delicious Burger 59 |
60 |
61 | 64 |
65 | ); 66 | }; 67 | 68 | export default HeroSection; 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✨ Features 2 | 3 | ## 👤 User Features 4 | 5 | ### 🔐 Authentication & Authorization 6 | - User Register & Login 7 | - Email Verification via **Mailtrap** 8 | - Forgot Password & Reset Password 9 | 10 | ### 📝 Profile Management 11 | - Update Profile Details 12 | 13 | ### 🔎 Search & Browse 14 | - Search Restaurants by **City, Country, or Restaurant Name** 15 | - See available **Cuisines** 16 | - View **Restaurant Details Page** 17 | 18 | ### 🛒 Cart & Orders 19 | - Add Food to Cart 20 | - Increase/Decrease Cart Quantity 21 | - Place Orders 22 | - Payment Integration with **Stripe Checkout** 23 | - Automatic Order Confirmation via **Stripe Webhook** 24 | - View User’s Past Orders 25 | 26 | --- 27 | 28 | ## 🛠️ Admin Features 29 | 30 | ### 🏢 Restaurant Management 31 | - Create One Restaurant 32 | - Edit Restaurant Details 33 | 34 | ### 🍴 Menu Management 35 | - Add Multiple Menus under Restaurant 36 | - Edit Menu Items 37 | - Delete Menus 38 | 39 | ### 📦 Order Management 40 | - View All Orders 41 | - Change Order Status (✅ Confirmed, 🍳 Preparing, 🚚 Out for Delivery, 📬 Delivered) 42 | 43 | --- 44 | 45 | ## 🔒 Security 46 | - Private Routes for **Users & Admins** 47 | - JWT Authentication with **Cookies** 48 | - Zod Validation for all input fields 49 | 50 | --- 51 | 52 | # 🏗️ Tech Stack 53 | 54 | ## 🎨 Frontend 55 | - ⚛️ React + TypeScript 56 | - 🎯 Redux Toolkit (State Management) 57 | - 🎨 Tailwind CSS + ShadCN UI 58 | 59 | ## 🚀 Backend 60 | - Express.js + TypeScript 61 | - MongoDB + Mongoose 62 | - Zod (Input Validation) 63 | - JWT Authentication 64 | 65 | ## 🔧 Others 66 | - 💳 Stripe Checkout + Webhooks (Payment & Order Confirmation) 67 | - 📧 Mailtrap (Email Verification & Reset Password) 68 | - ☁️ Cloudinary (Image Upload & Management) 69 | 70 | --- 71 | 72 | # 📸 Screens & Flows 73 | 74 | - 🔐 **Auth Flow** → Register → Verify Email → Login → Forgot/Reset Password 75 | - 🏠 **User Flow** → Search Restaurants → View Cuisines → Add to Cart → Checkout → Payment → Order Tracking 76 | - 🛠️ **Admin Flow** → Create Restaurant → Add Menus → Manage Menus → Manage Orders 77 | 78 | --- 79 | 80 | # ⚙️ Installation 81 | 82 | ### 1️⃣ Clone Repo 83 | ```bash 84 | git clone https://github.com/shariyerShazan/MealMart-restaurant-client 85 | cd mealmart 86 | 87 | 88 | 89 | 90 | ## backend 91 | # Event Explorer Backend 92 | 93 | This is the **backend server** for the Event Explorer application. 94 | It is built with **Node.js**, **Express.js**, and **MongoDB**. 95 | The backend provides REST APIs to manage events, categories, cuisines, and user interactions. 96 | 97 | --- 98 | 99 | ## 🚀 Features 100 | - Event management (CRUD operations) 101 | - Category management 102 | - Cuisine management 103 | - User authentication (JWT based) 104 | - MongoDB database integration with Mongoose 105 | - Clean and modular project structure 106 | 107 | --- 108 | ### 1️ Clone Repo 109 | git clone https://github.com/shariyerShazan/MealMart-restaurant-server 110 | cd server 111 | 112 | -------------------------------------------------------------------------------- /src/routes/Routes.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from "react-router"; 2 | import MainLayout from "../layouts/MainLayout"; 3 | import Home from "../pages/Home"; 4 | import Login from "../auth/Login"; 5 | import Register from "../auth/Register"; 6 | import ForgotPassword from "../auth/ForgotPassword"; 7 | import ResetPassword from "../auth/ResetPassword"; 8 | import VerifyEmail from "../auth/VerifyEmail"; 9 | import Profile from "../pages/Profile"; 10 | import SearchPage from "../pages/SearchPage"; 11 | import RestaurantDetails from "../pages/RestaurantDetails"; 12 | import Cart from "../pages/Cart"; 13 | import DashboardLayout from "../layouts/DashboardLayout"; 14 | import Dashboard from "../pages/Admin/Dashboard"; 15 | import Reastaurant from "../pages/Admin/Reastaurant"; 16 | import Menus from "../pages/Admin/Menus"; 17 | import UserOrder from "../pages/UserOrder"; 18 | import PrivateRoute from "./PrivateRoute"; 19 | 20 | 21 | const Router = createBrowserRouter([ 22 | { 23 | path: "/" , 24 | element: , 25 | children: [ 26 | { 27 | index: true , 28 | element: 29 | } , 30 | { 31 | path: "profile" , 32 | element: 33 | } , 34 | { 35 | path: "search/:searchText", 36 | element: 37 | } , 38 | { 39 | path: "restaurant/:restaurantId" , 40 | element: 41 | } , 42 | { 43 | path: "cart" , 44 | element : 45 | }, 46 | { 47 | path: "order" , 48 | element : 49 | } 50 | ] 51 | }, 52 | { 53 | path: "/dashboard" , 54 | element: 55 | ( 56 | 57 | ), 58 | 59 | children: [ 60 | { 61 | index: true , 62 | element: 63 | 64 | 65 | 66 | 67 | } , 68 | { 69 | path : "restaurant" , 70 | element: 71 | 72 | 73 | 74 | } , 75 | { 76 | path: "add-menu" , 77 | element: 78 | } 79 | ] 80 | }, 81 | { 82 | path: "/login" , 83 | element: 84 | }, 85 | { 86 | path: "/register" , 87 | element: 88 | }, 89 | { 90 | path: "/forgot-password" , 91 | element: 92 | }, 93 | { 94 | path: "/reset-password" , 95 | element: 96 | }, 97 | { 98 | path: "/verify-email" , 99 | element: 100 | } 101 | 102 | ]) 103 | 104 | export default Router -------------------------------------------------------------------------------- /src/components/shared/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { FaFacebookF, FaTwitter, FaInstagram, FaLinkedinIn } from "react-icons/fa"; 3 | 4 | const Footer: React.FC = () => { 5 | const [email, setEmail] = useState(""); 6 | 7 | const handleSubscribe = () => { 8 | if(email.trim()) { 9 | alert(`Subscribed with ${email}`); 10 | setEmail(""); 11 | } 12 | }; 13 | 14 | return ( 15 |
16 |
17 |
18 | 19 | {/* Logo & Description */} 20 |
21 |

22 | MealMart 23 |

24 |

25 | Discover and enjoy delicious meals from your favorite restaurants. We bring the best dishes to your doorstep. 26 |

27 | 28 | {/* Social Icons */} 29 |
30 | 31 | 32 | 33 | 34 |
35 |
36 | 37 | {/* Quick Links */} 38 |
39 |

Quick Links

40 | 47 |
48 | 49 | {/* Newsletter */} 50 |
51 |

Subscribe to our Newsletter

52 |

53 | Get latest updates about new restaurants, offers, and promotions. 54 |

55 |
56 | setEmail(e.target.value)} 61 | className="flex-1 px-4 py-2 rounded-l-full border border-myColor text-gray-800" 62 | /> 63 | 69 |
70 |
71 | 72 |
73 | 74 | {/* Divider */} 75 |
76 | © {new Date().getFullYear()} MealMart. All rights reserved. 77 |
78 |
79 |
80 | ); 81 | }; 82 | 83 | export default Footer; 84 | -------------------------------------------------------------------------------- /src/pages/UserOrder.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useEffect, useState } from 'react'; 3 | import { ORDER_API_END_POINT } from '../utils/apiEndPoint'; 4 | 5 | // ✅ Type Definitions 6 | interface User { 7 | fullName: string; 8 | email?: string; 9 | contact?: string; 10 | } 11 | 12 | interface CartItem { 13 | menuId: string; 14 | foodName: string; 15 | price: number; 16 | foodImage: string; 17 | quantity: number; 18 | } 19 | 20 | interface Order { 21 | _id: string; 22 | orderBy: User; 23 | cartItems: CartItem[]; 24 | totalAmount: number; 25 | status: string; 26 | } 27 | 28 | const UserOrder: React.FC = () => { 29 | const [orders, setOrders] = useState([]); 30 | const [loading, setLoading] = useState(true); 31 | 32 | useEffect(() => { 33 | document.title = `Order | MealMart`; 34 | }, []); 35 | 36 | useEffect(() => { 37 | const fetchOrders = async () => { 38 | try { 39 | const res = await axios.get<{ success: boolean; orders: Order[]; message?: string }>( 40 | `${ORDER_API_END_POINT}`, 41 | { withCredentials: true } 42 | ); 43 | 44 | if (res.data.success) { 45 | setOrders(res.data.orders); 46 | } else { 47 | console.log(res.data.message); 48 | } 49 | } catch (error) { 50 | console.log(error); 51 | } finally { 52 | setLoading(false); 53 | } 54 | }; 55 | 56 | fetchOrders(); 57 | }, []); 58 | 59 | if (loading) { 60 | return ( 61 |
62 | Loading... 63 |
64 | ); 65 | } 66 | 67 | if (orders.length === 0) { 68 | return ( 69 |
70 | No order found! 71 |
72 | ); 73 | } 74 | 75 | return ( 76 |
77 |

78 | Your Orders 79 |

80 | 81 |
82 | {orders.map((order, index) => ( 83 |
84 |

{order.orderBy.fullName}

85 |

Order summary:

86 | 87 | {order.cartItems.map((item, idx) => ( 88 |
92 |
93 | {item.foodName} 98 |

{item.foodName}

99 |
100 |

101 | ${item.price}*{item.quantity} = ${item.quantity * item.price} 102 |

103 |
104 | ))} 105 | 106 |

107 | Total Amount 108 | ${order.totalAmount} 109 |

110 | 111 |

112 | Order Status:{' '} 113 | {order.status} 114 |

115 |
116 | ))} 117 |
118 |
119 | ); 120 | }; 121 | 122 | export default UserOrder; 123 | -------------------------------------------------------------------------------- /src/assets 00-49-08-258/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DialogPrimitive from "@radix-ui/react-dialog" 3 | import { XIcon } from "lucide-react" 4 | 5 | import { cn } from "../../lib/utils" 6 | 7 | function Dialog({ 8 | ...props 9 | }: React.ComponentProps) { 10 | return 11 | } 12 | 13 | function DialogTrigger({ 14 | ...props 15 | }: React.ComponentProps) { 16 | return 17 | } 18 | 19 | function DialogPortal({ 20 | ...props 21 | }: React.ComponentProps) { 22 | return 23 | } 24 | 25 | function DialogClose({ 26 | ...props 27 | }: React.ComponentProps) { 28 | return 29 | } 30 | 31 | function DialogOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 44 | ) 45 | } 46 | 47 | function DialogContent({ 48 | className, 49 | children, 50 | showCloseButton = true, 51 | ...props 52 | }: React.ComponentProps & { 53 | showCloseButton?: boolean 54 | }) { 55 | return ( 56 | 57 | 58 | 66 | {children} 67 | {showCloseButton && ( 68 | 72 | 73 | Close 74 | 75 | )} 76 | 77 | 78 | ) 79 | } 80 | 81 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 82 | return ( 83 |
88 | ) 89 | } 90 | 91 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 92 | return ( 93 |
101 | ) 102 | } 103 | 104 | function DialogTitle({ 105 | className, 106 | ...props 107 | }: React.ComponentProps) { 108 | return ( 109 | 114 | ) 115 | } 116 | 117 | function DialogDescription({ 118 | className, 119 | ...props 120 | }: React.ComponentProps) { 121 | return ( 122 | 127 | ) 128 | } 129 | 130 | export { 131 | Dialog, 132 | DialogClose, 133 | DialogContent, 134 | DialogDescription, 135 | DialogFooter, 136 | DialogHeader, 137 | DialogOverlay, 138 | DialogPortal, 139 | DialogTitle, 140 | DialogTrigger, 141 | } 142 | -------------------------------------------------------------------------------- /src/auth/VerifyEmail.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, type FormEvent } from 'react' 2 | import { Input } from '../components/ui/input' 3 | // import { useNavigate } from 'react-router' 4 | import { Button } from '../components/ui/button' 5 | import { Loader2 } from 'lucide-react' 6 | import axios from 'axios' 7 | import { USER_API_END_POINT } from '../utils/apiEndPoint' 8 | import { useNavigate, type NavigateFunction } from 'react-router' 9 | import { toast } from 'react-toastify' 10 | 11 | function VerifyEmail() { 12 | 13 | const navigate : NavigateFunction = useNavigate() 14 | const [otp, setOtp] = useState(["", "", "", "", "", ""]) 15 | const inputRef = useRef([]) 16 | // const navigate = useNavigate() 17 | const [isLoading , setIsLoading] = useState(false) 18 | 19 | const handleChange = (index: number, value: string) => { 20 | if (/^[A-Za-z0-9]$/.test(value) || value === "") { 21 | const newOtp = [...otp] 22 | newOtp[index] = value 23 | setOtp(newOtp) 24 | if (value && index < 5) { 25 | inputRef.current[index + 1]?.focus() 26 | } 27 | } 28 | } 29 | 30 | const handleKeyDown = (e: React.KeyboardEvent, index: number) => { 31 | if (e.key === "Backspace" && !otp[index] && index > 0) { 32 | inputRef.current[index - 1]?.focus() 33 | } 34 | } 35 | 36 | const handlePaste = (e: React.ClipboardEvent) => { 37 | e.preventDefault() 38 | const pasteData = e.clipboardData.getData('text').slice(0, 6) 39 | if (/^[A-Za-z0-9]+$/.test(pasteData)) { 40 | const newOtp = pasteData.split("") 41 | setOtp([...newOtp, ...Array(6 - newOtp.length).fill("")].slice(0, 6)) 42 | inputRef.current[Math.min(pasteData.length - 1, 5)]?.focus() 43 | } 44 | } 45 | const handleVerify = async (e:FormEvent)=>{ 46 | e.preventDefault() 47 | setIsLoading(true) 48 | try { 49 | const verificationCode = otp.join("") 50 | const res = await axios.post(`${USER_API_END_POINT}/verify-email` , {verificationCode} , {withCredentials: true}) 51 | if(res.data.success){ 52 | navigate("/login") 53 | setIsLoading(false) 54 | toast.success(res.data.message) 55 | } 56 | } catch (error: any) { 57 | toast.error(error?.response?.data?.message) 58 | console.log(error) 59 | setIsLoading(false) 60 | } 61 | } 62 | 63 | return ( 64 |
65 |
66 |
67 |

Verify your email

68 |

Enter the 6 digit code sent to your email address

69 |
70 | { 71 | otp.map((letter: string, index: number) => ( 72 | { if (element) inputRef.current[index] = element }} 78 | maxLength={1} 79 | onChange={(e) => handleChange(index, e.target.value)} 80 | onKeyDown={(e) => handleKeyDown(e, index)} 81 | onPaste={handlePaste} 82 | /> 83 | )) 84 | } 85 |
86 | { 87 | isLoading ? : 92 | } 93 |
94 |
95 |
96 | ) 97 | } 98 | 99 | export default VerifyEmail 100 | -------------------------------------------------------------------------------- /src/pages/Admin/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useAppDispatch, useAppSelector } from '../../hooks/useReduxTypeHooks'; 3 | import useGetRestOrders from '../../hooks/apiHooks/useGetRestOrders'; 4 | import axios from 'axios'; 5 | import { RESTAURANT_API_END_POINT } from '../../utils/apiEndPoint'; 6 | import { setOrders } from '../../redux/restaurantSlice'; 7 | 8 | const Dashboard = () => { 9 | 10 | useEffect(() => { 11 | document.title = `Dashboard | MealMart`; 12 | }, []); 13 | 14 | const { orders } = useAppSelector((state) => state.restaurant); 15 | const [dependency, setDependency] = useState(false); 16 | const dispatch = useAppDispatch(); 17 | 18 | useGetRestOrders({ dependency }); 19 | 20 | useEffect(() => { 21 | setDependency(true); 22 | }, []); 23 | 24 | const handleStatusChange = async (orderId: string, newStatus: string) => { 25 | try { 26 | const res = await axios.patch( 27 | `${RESTAURANT_API_END_POINT}/order/status/${orderId}`, 28 | { status: newStatus }, 29 | { withCredentials: true } 30 | ); 31 | 32 | if (res.data.success) { 33 | dispatch( 34 | setOrders( 35 | orders.map((order) => 36 | order._id === orderId ? { ...order, status: res.data.status } : order 37 | ) 38 | ) 39 | ); 40 | setDependency((prev) => !prev); // re-fetch if needed 41 | } 42 | } catch (error) { 43 | console.error('Failed to update order status', error); 44 | } 45 | }; 46 | 47 | // safeguard for empty or invalid orders 48 | if (!Array.isArray(orders) || orders.length === 0) { 49 | return ( 50 |
51 |

No order Found

52 |
53 | ); 54 | } 55 | 56 | return ( 57 |
58 |

59 | Orders Overview 60 |

61 |
62 | {orders.map((order: any, index: number) => ( 63 |
64 |

{order?.orderBy?.fullName}

65 |

66 | Address: {order.deliveryInfo.address} 67 |

68 |

69 | City: {order.deliveryInfo.city} 70 |

71 |

72 | Total Amount: ${order.totalAmount} 73 |

74 | 75 | {order?.cartItems?.map((item: any, idx: number) => ( 76 |
80 |
81 | 86 |

{item?.foodName}

87 |
88 |

89 | ${item?.price}*{item?.quantity} = ${item?.quantity * item?.price} 90 |

91 |
92 | ))} 93 | 94 |
95 | 96 |
97 | 107 |
108 |
109 | ))} 110 |
111 |
112 | ); 113 | }; 114 | 115 | export default Dashboard; 116 | -------------------------------------------------------------------------------- /src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SheetPrimitive from "@radix-ui/react-dialog" 3 | import { XIcon } from "lucide-react" 4 | import { cn } from "../../lib/utils" 5 | 6 | 7 | 8 | function Sheet({ ...props }: React.ComponentProps) { 9 | return 10 | } 11 | 12 | function SheetTrigger({ 13 | ...props 14 | }: React.ComponentProps) { 15 | return 16 | } 17 | 18 | function SheetClose({ 19 | ...props 20 | }: React.ComponentProps) { 21 | return 22 | } 23 | 24 | function SheetPortal({ 25 | ...props 26 | }: React.ComponentProps) { 27 | return 28 | } 29 | 30 | function SheetOverlay({ 31 | className, 32 | ...props 33 | }: React.ComponentProps) { 34 | return ( 35 | 43 | ) 44 | } 45 | 46 | function SheetContent({ 47 | className, 48 | children, 49 | side = "right", 50 | ...props 51 | }: React.ComponentProps & { 52 | side?: "top" | "right" | "bottom" | "left" 53 | }) { 54 | return ( 55 | 56 | 57 | 73 | {children} 74 | 75 | 76 | Close 77 | 78 | 79 | 80 | ) 81 | } 82 | 83 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { 84 | return ( 85 |
90 | ) 91 | } 92 | 93 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { 94 | return ( 95 |
100 | ) 101 | } 102 | 103 | function SheetTitle({ 104 | className, 105 | ...props 106 | }: React.ComponentProps) { 107 | return ( 108 | 113 | ) 114 | } 115 | 116 | function SheetDescription({ 117 | className, 118 | ...props 119 | }: React.ComponentProps) { 120 | return ( 121 | 126 | ) 127 | } 128 | 129 | export { 130 | Sheet, 131 | SheetTrigger, 132 | SheetClose, 133 | SheetContent, 134 | SheetHeader, 135 | SheetFooter, 136 | SheetTitle, 137 | SheetDescription, 138 | } 139 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --radius-sm: calc(var(--radius) - 4px); 8 | --radius-md: calc(var(--radius) - 2px); 9 | --radius-lg: var(--radius); 10 | --radius-xl: calc(var(--radius) + 4px); 11 | --color-background: var(--background); 12 | --color-foreground: var(--foreground); 13 | --color-card: var(--card); 14 | --color-card-foreground: var(--card-foreground); 15 | --color-popover: var(--popover); 16 | --color-popover-foreground: var(--popover-foreground); 17 | --color-primary: var(--primary); 18 | --color-primary-foreground: var(--primary-foreground); 19 | --color-secondary: var(--secondary); 20 | --color-secondary-foreground: var(--secondary-foreground); 21 | --color-muted: var(--muted); 22 | --color-muted-foreground: var(--muted-foreground); 23 | --color-accent: var(--accent); 24 | --color-accent-foreground: var(--accent-foreground); 25 | --color-destructive: var(--destructive); 26 | --color-border: var(--border); 27 | --color-input: var(--input); 28 | --color-ring: var(--ring); 29 | --color-chart-1: var(--chart-1); 30 | --color-chart-2: var(--chart-2); 31 | --color-chart-3: var(--chart-3); 32 | --color-chart-4: var(--chart-4); 33 | --color-chart-5: var(--chart-5); 34 | --color-sidebar: var(--sidebar); 35 | --color-sidebar-foreground: var(--sidebar-foreground); 36 | --color-sidebar-primary: var(--sidebar-primary); 37 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 38 | --color-sidebar-accent: var(--sidebar-accent); 39 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 40 | --color-sidebar-border: var(--sidebar-border); 41 | --color-sidebar-ring: var(--sidebar-ring); 42 | } 43 | 44 | :root { 45 | --radius: 0.625rem; 46 | --background: oklch(1 0 0); 47 | --foreground: oklch(0.145 0 0); 48 | --card: oklch(1 0 0); 49 | --card-foreground: oklch(0.145 0 0); 50 | --popover: oklch(1 0 0); 51 | --popover-foreground: oklch(0.145 0 0); 52 | --primary: oklch(0.205 0 0); 53 | --primary-foreground: oklch(0.985 0 0); 54 | --secondary: oklch(0.97 0 0); 55 | --secondary-foreground: oklch(0.205 0 0); 56 | --muted: oklch(0.97 0 0); 57 | --muted-foreground: oklch(0.556 0 0); 58 | --accent: oklch(0.97 0 0); 59 | --accent-foreground: oklch(0.205 0 0); 60 | --destructive: oklch(0.577 0.245 27.325); 61 | --border: oklch(0.922 0 0); 62 | --input: oklch(0.922 0 0); 63 | --ring: oklch(0.708 0 0); 64 | --chart-1: oklch(0.646 0.222 41.116); 65 | --chart-2: oklch(0.6 0.118 184.704); 66 | --chart-3: oklch(0.398 0.07 227.392); 67 | --chart-4: oklch(0.828 0.189 84.429); 68 | --chart-5: oklch(0.769 0.188 70.08); 69 | --sidebar: oklch(0.985 0 0); 70 | --sidebar-foreground: oklch(0.145 0 0); 71 | --sidebar-primary: oklch(0.205 0 0); 72 | --sidebar-primary-foreground: oklch(0.985 0 0); 73 | --sidebar-accent: oklch(0.97 0 0); 74 | --sidebar-accent-foreground: oklch(0.205 0 0); 75 | --sidebar-border: oklch(0.922 0 0); 76 | --sidebar-ring: oklch(0.708 0 0); 77 | } 78 | 79 | .dark { 80 | --background: oklch(0.145 0 0); 81 | --foreground: oklch(0.985 0 0); 82 | --card: oklch(0.205 0 0); 83 | --card-foreground: oklch(0.985 0 0); 84 | --popover: oklch(0.205 0 0); 85 | --popover-foreground: oklch(0.985 0 0); 86 | --primary: oklch(0.922 0 0); 87 | --primary-foreground: oklch(0.205 0 0); 88 | --secondary: oklch(0.269 0 0); 89 | --secondary-foreground: oklch(0.985 0 0); 90 | --muted: oklch(0.269 0 0); 91 | --muted-foreground: oklch(0.708 0 0); 92 | --accent: oklch(0.269 0 0); 93 | --accent-foreground: oklch(0.985 0 0); 94 | --destructive: oklch(0.704 0.191 22.216); 95 | --border: oklch(1 0 0 / 10%); 96 | --input: oklch(1 0 0 / 15%); 97 | --ring: oklch(0.556 0 0); 98 | --chart-1: oklch(0.488 0.243 264.376); 99 | --chart-2: oklch(0.696 0.17 162.48); 100 | --chart-3: oklch(0.769 0.188 70.08); 101 | --chart-4: oklch(0.627 0.265 303.9); 102 | --chart-5: oklch(0.645 0.246 16.439); 103 | --sidebar: oklch(0.205 0 0); 104 | --sidebar-foreground: oklch(0.985 0 0); 105 | --sidebar-primary: oklch(0.488 0.243 264.376); 106 | --sidebar-primary-foreground: oklch(0.985 0 0); 107 | --sidebar-accent: oklch(0.269 0 0); 108 | --sidebar-accent-foreground: oklch(0.985 0 0); 109 | --sidebar-border: oklch(1 0 0 / 10%); 110 | --sidebar-ring: oklch(0.556 0 0); 111 | } 112 | 113 | @layer base { 114 | * { 115 | @apply border-border outline-ring/50; 116 | } 117 | body { 118 | @apply bg-background text-foreground; 119 | } 120 | } 121 | 122 | 123 | 124 | @theme { 125 | --color-myColor: #fd6640 ; 126 | --color-myColortwo: #1976d2 ; 127 | } 128 | 129 | -------------------------------------------------------------------------------- /src/pages/Admin/Menus.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Button } from "../../components/ui/button"; 3 | import MenuDialog from "../../components/shared/Admin/MenuDialog"; 4 | import { useAppSelector } from "../../hooks/useReduxTypeHooks"; 5 | import useGetRestaurant from "../../hooks/apiHooks/useGetRestaurant"; 6 | import axios from "axios"; 7 | import { MENU_API_END_POINT } from "../../utils/apiEndPoint"; 8 | import { toast } from "react-toastify"; 9 | import { Loader2 } from "lucide-react"; 10 | 11 | 12 | interface MenuItem { 13 | _id: string ; 14 | foodName: string; 15 | description: string; 16 | price: number; 17 | foodImage: string; 18 | } 19 | 20 | const Menus = () => { 21 | 22 | useEffect(() => { 23 | document.title = `Dashboard | MealMart`; 24 | }, []); 25 | 26 | const [addOne, setAddOne]= useState(false) 27 | const[deleteLoading , setDeleteLoading] = useState(false) 28 | 29 | useGetRestaurant({dependency:addOne}) 30 | 31 | const { restaurant} = useAppSelector((state)=> state.restaurant) 32 | const menus = restaurant?.menus 33 | const [openDialog, setOpenDialog] = useState(false); 34 | const [editMenu, setEditMenu] = useState(null); 35 | 36 | const handleDelete = async (menuId: string)=>{ 37 | setDeleteLoading(true) 38 | try { 39 | const res = await axios.delete(`${MENU_API_END_POINT}/${menuId}` , {withCredentials: true}) 40 | if(res.data.success){ 41 | toast.success(res.data.messaage) 42 | setAddOne(true) 43 | setDeleteLoading(false) 44 | } 45 | } catch (error:any) { 46 | toast(error.response.data.messaage) 47 | console.log(error) 48 | setDeleteLoading(false) 49 | } 50 | } 51 | 52 | return ( 53 |
54 | 55 |
56 |

Available Menus

57 | 66 |
67 | 68 |
69 | { menus?.length === 0 ? 70 |
71 |

No Menu Found, Please Add First

72 |
73 | : menus?.map((menu : MenuItem, index:number) => ( 74 |
75 | {menu?.foodName} 80 |
81 |

{menu?.foodName}

82 |

{menu?.description}

83 |

84 | Price: ${menu?.price} 85 |

86 |
87 | 97 | { 98 | deleteLoading? : 101 | 107 | } 108 |
109 |
110 |
111 | ))} 112 |
113 | 114 | setOpenDialog(false)} 118 | defaultValues={editMenu || undefined} 119 | /> 120 |
121 | ); 122 | }; 123 | 124 | export default Menus; 125 | -------------------------------------------------------------------------------- /src/pages/Cart.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from "react"; 2 | import { FaMinus, FaPlus } from "react-icons/fa"; 3 | import { useAppDispatch, useAppSelector } from "../hooks/useReduxTypeHooks"; 4 | import { 5 | clearCart, 6 | decreaseQuantity, 7 | deleteFromCart, 8 | increaseQuantity, 9 | } from "../redux/cartSlice"; 10 | import CheckoutDialog from "../components/shared/CheckoutDialog"; 11 | 12 | 13 | const Cart = () => { 14 | 15 | useEffect(() => { 16 | document.title = `Cart | MealMart`; 17 | }, []); 18 | 19 | 20 | const { foods } = useAppSelector((state) => state.cart); 21 | const dispatch = useAppDispatch(); 22 | 23 | // total price calculation 24 | const totalPrice = useMemo(() => { 25 | return foods.reduce((sum, item) => sum + item.price * item.quantity, 0); 26 | }, [foods]); 27 | 28 | const handleIncrease = (foodId: string) => { 29 | dispatch(increaseQuantity(foodId)); 30 | }; 31 | 32 | const handleDecrease = (foodId: string) => { 33 | dispatch(decreaseQuantity(foodId)); 34 | }; 35 | 36 | const handleRemove = (foodId: string) => { 37 | dispatch(deleteFromCart(foodId)); 38 | }; 39 | 40 | const handleClearAll = () => { 41 | dispatch(clearCart()); 42 | }; 43 | 44 | return ( 45 |
46 | {/* Clear All */} 47 |
48 | 54 |
55 | 56 | {/* Table Wrapper */} 57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | {foods && 71 | foods.map((item) => ( 72 | 73 | 80 | 81 | 82 | 97 | 100 | 108 | 109 | ))} 110 | 111 | {foods?.length === 0 && ( 112 | 113 | 116 | 117 | )} 118 | 119 |
ItemTitlePriceQuantityTotalAction
74 | {item.foodName} 79 | {item.foodName}${item.price} 83 | 89 | {item.quantity} 90 | 96 | 98 | ${item.price * item.quantity} 99 | 101 | 107 |
114 | Cart is empty 115 |
120 | 121 | {/* Total + Checkout */} 122 |
123 |

Total Price: ${totalPrice}

124 | 125 |
126 |
127 |
128 | ); 129 | }; 130 | 131 | export default Cart; 132 | -------------------------------------------------------------------------------- /src/pages/SearchPage.tsx: -------------------------------------------------------------------------------- 1 | // SearchPage.tsx 2 | import React, { useEffect, useState, type ChangeEvent } from "react"; 3 | import { FiSearch, FiX } from "react-icons/fi"; 4 | import RestaurantCard from "../components/shared/RestaurantCard"; 5 | import FilterOptions from "../components/shared/FilterOptions"; 6 | import { useAppSelector } from "../hooks/useReduxTypeHooks"; 7 | import useGetAllRestaurant from "../hooks/apiHooks/useGetAllRestaurant"; 8 | import { useNavigate, useParams } from "react-router"; 9 | 10 | const SearchPage: React.FC = () => { 11 | 12 | useEffect(() => { 13 | document.title = `Reastaurant | MealMart`; 14 | }, []); 15 | 16 | 17 | const navigate = useNavigate() 18 | const { searchText } = useParams(); 19 | const [searchCusines, setSearchCuisines] = useState(""); 20 | const [filters, setFilters] = useState([]); 21 | 22 | const [noTwo , setNoTwo] = useState("") 23 | 24 | const { allRestaurant } = useAppSelector((state) => state.restaurant); 25 | 26 | useGetAllRestaurant({ 27 | searchText, 28 | cuisines: filters.join(",").toLocaleLowerCase() || searchCusines, 29 | // dependency: searchText + filters.join(","), 30 | }); 31 | 32 | const handleSearch = () => { 33 | // console.log("Search triggered for:", searchText, filters); 34 | }; 35 | 36 | const handleRemoveFilter = (filter: string) => { 37 | setFilters(filters.filter((f) => f !== filter)); 38 | }; 39 | 40 | const handleClearFilters = () => { 41 | setFilters([]); 42 | }; 43 | const handleSearchTwo = ()=>{ 44 | navigate(`/search/${noTwo}`) 45 | } 46 | return ( 47 |
48 | {/* Left - Filters */} 49 | 53 | 54 | {/* Right - Content */} 55 |
56 | {/* Search bar */} 57 |
58 |
59 | ) => setSearchCuisines(e.target.value)} 64 | className="flex-1 px-4 py-2 outline-none w-full " 65 | /> 66 | 72 | 73 |
74 |
75 | )=>setNoTwo(e.target.value)} 80 | className="flex-1 px-4 py-2 outline-none w-full" 81 | /> 82 | 88 | 89 |
90 | 91 |
92 | {/* Results info */} 93 |
94 | 95 | ({allRestaurant?.length || 0}) restaurants found {allRestaurant && `in ${searchText}`} 96 | 97 | 98 | {/* Active filters */} 99 | {filters.map((filter) => ( 100 |
104 | {filter} 105 | 111 |
112 | ))} 113 | 114 | {/* Clear all */} 115 | {filters.length > 0 && ( 116 | 122 | )} 123 |
124 | 125 | {/* Restaurant cards */} 126 |
127 | {allRestaurant?.length > 0 ? ( 128 | allRestaurant.map((rest, index) => ) 129 | ) : ( 130 |

No restaurants found

131 | )} 132 |
133 |
134 |
135 | ); 136 | }; 137 | 138 | export default SearchPage; 139 | -------------------------------------------------------------------------------- /src/auth/Login.tsx: -------------------------------------------------------------------------------- 1 | import { useState, type ChangeEvent, type FormEvent } from 'react' 2 | import { MdLockOutline, MdOutlineEmail } from "react-icons/md"; 3 | import { Input } from '../components/ui/input' 4 | import { Label } from '@radix-ui/react-label' 5 | import { IoMdEye, IoMdEyeOff } from "react-icons/io"; 6 | import { Button } from '../components/ui/button'; 7 | import { Link, useNavigate, type NavigateFunction } from 'react-router'; 8 | import { Loader2 } from 'lucide-react'; 9 | import { userLoginSchema, type loginInputState } from '../schemaZOD/userSchem'; 10 | import axios from 'axios'; 11 | import { USER_API_END_POINT } from '../utils/apiEndPoint'; 12 | import { toast } from 'react-toastify'; 13 | import { useAppDispatch } from '../hooks/useReduxTypeHooks'; 14 | import { setUser } from '../redux/userSlice'; 15 | 16 | 17 | function Login() { 18 | const navigate: NavigateFunction = useNavigate() 19 | const dispatch = useAppDispatch() 20 | const [isVisible, setIsVisible] = useState(false) 21 | const [isLoading, setIsLoading] = useState(false) 22 | 23 | const [input, setInput] = useState({ 24 | email: "", 25 | password: "" 26 | }) 27 | 28 | const changeEventHandler = (e: ChangeEvent) => { 29 | const { name, value } = e.target 30 | setInput({ ...input, [name]: value }) 31 | } 32 | 33 | const [error, setError] = useState>({}) 34 | 35 | const loginSubmitHandler = async (e: FormEvent) => { 36 | e.preventDefault() 37 | setIsLoading(true) 38 | const result = userLoginSchema.safeParse(input) 39 | if (!result.success) { 40 | setIsLoading(false) 41 | setError(result.error.flatten().fieldErrors as Partial) 42 | return; 43 | } 44 | try { 45 | const res = await axios.post(`${USER_API_END_POINT}/login`, input, { withCredentials: true }) 46 | if (res.data.success) { 47 | toast.success(res.data.message) 48 | dispatch(setUser(res.data.user)) 49 | setIsLoading(false) 50 | navigate("/") 51 | } 52 | } catch (error: any) { 53 | setIsLoading(false) 54 | toast.error(error?.response?.data?.message) 55 | } finally { 56 | setIsLoading(false) 57 | } 58 | } 59 | 60 | // 🔹 Admin Login Handler 61 | const handleAdminLogin = () => { 62 | setInput({ 63 | email: "shazan@gmail.com", 64 | password: "shazan1" 65 | }) 66 | } 67 | 68 | return ( 69 |
70 |
71 |
72 |

MealMart Login

73 |
74 |
75 | 76 |
77 | 84 |
85 | {error && {error.email}} 86 | 87 |
88 |
89 |
90 | 97 | {isVisible 98 | ? setIsVisible(!isVisible)} size={20} className=' absolute right-0 inset-y-9 mr-2 cursor-pointer' /> 99 | : setIsVisible(!isVisible)} size={20} className=' absolute right-0 inset-y-9 mr-2 cursor-pointer' />} 100 |
101 | {error && {error.password}} 102 | 103 |
104 | { 105 | isLoading ? : 110 | } 111 | 112 | {/* 🔹 Admin Login Button */} 113 | 116 | 117 |
118 | 119 | Forgot password 120 | 121 |
122 |

123 | Don't have account? Register 124 |

125 |
126 |
127 | 128 |
129 |
130 | ) 131 | } 132 | 133 | export default Login 134 | -------------------------------------------------------------------------------- /src/components/shared/Admin/MenuDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../../ui/dialog"; 3 | import { Button } from "../../ui/button"; 4 | import { toast } from "react-toastify"; 5 | import { Avatar, AvatarFallback, AvatarImage } from "../../ui/avatar"; 6 | import { FiPlus } from "react-icons/fi"; 7 | import { Loader2 } from "lucide-react"; 8 | import axios from "axios"; 9 | import { MENU_API_END_POINT } from "../../../utils/apiEndPoint"; 10 | 11 | interface MenuDialogProps { 12 | isOpen: boolean; 13 | onClose: () => void; 14 | defaultValues?: any; 15 | setAddOne: (val: boolean) => void; 16 | } 17 | 18 | const MenuDialog: React.FC = ({ isOpen, onClose, defaultValues, setAddOne }) => { 19 | const imageRef = useRef(null); 20 | const [loading, setIsLoading] = useState(false); 21 | const [preview, setPreview] = useState(""); 22 | const [data, setData] = useState({ 23 | foodName: "", 24 | description: "", 25 | price: "", 26 | foodImage: "" 27 | }); 28 | 29 | useEffect(() => { 30 | if (defaultValues) { 31 | setData(defaultValues); 32 | setPreview(defaultValues.foodImage || ""); 33 | } 34 | }, [defaultValues]); 35 | 36 | const handleChange = (e: React.ChangeEvent) => { 37 | const { name, value } = e.target; 38 | setData({ ...data, [name]: value }); 39 | }; 40 | 41 | const handleImageChange = (e: React.ChangeEvent) => { 42 | if (e.target.files && e.target.files[0]) { 43 | const file = e.target.files[0]; 44 | setPreview(URL.createObjectURL(file)); 45 | } 46 | }; 47 | 48 | const handleSubmit = async () => { 49 | setIsLoading(true); 50 | const formData = new FormData(); 51 | formData.append("foodName", data?.foodName); 52 | formData.append("description", data?.description.slice(0 , 200)); 53 | formData.append("price", data?.price); 54 | 55 | const fileInput = imageRef.current?.files?.[0]; 56 | if (fileInput && fileInput.size > 5 * 1024 * 1024) { 57 | setIsLoading(false); 58 | toast.error("File size should be less than 5MB"); 59 | return; 60 | } 61 | if (fileInput) { 62 | formData.append("foodImage", fileInput); 63 | } 64 | 65 | try { 66 | let res; 67 | if (defaultValues && defaultValues._id) { 68 | // Update existing menu 69 | res = await axios.patch(`${MENU_API_END_POINT}/${defaultValues._id}`, formData, { withCredentials: true }); 70 | } else { 71 | // Add new menu 72 | res = await axios.post(MENU_API_END_POINT, formData, { withCredentials: true }); 73 | } 74 | 75 | if (res.data.success) { 76 | setAddOne(true); // trigger parent refresh 77 | setIsLoading(false); 78 | toast.success(res.data.message || "Operation successful"); 79 | onClose(); 80 | } 81 | } catch (error: any) { 82 | console.log(error); 83 | setIsLoading(false); 84 | toast.error(error?.response?.data?.message || "Something went wrong"); 85 | } 86 | }; 87 | 88 | return ( 89 | 90 | 91 | 92 | {defaultValues ? "Edit Menu" : "Add Menu"} 93 | 94 | 95 |
96 | 97 | 105 | 106 | 114 | 115 |