├── client ├── src │ ├── App.css │ ├── containers │ │ └── LoggedInContainer.js │ ├── assets │ │ └── images │ │ │ ├── Name.png │ │ │ ├── drone.jpg │ │ │ ├── store.png │ │ │ ├── watch.png │ │ │ ├── logoBig.png │ │ │ ├── Logo_black.png │ │ │ ├── Sparkthon.png │ │ │ ├── uploadIcon.png │ │ │ ├── watchBlack.png │ │ │ ├── watchBlue.png │ │ │ ├── watchGreen.png │ │ │ ├── watchsmol.png │ │ │ ├── decor.svg │ │ │ └── ondc.svg │ ├── utils │ │ ├── config.js │ │ ├── isAuthenticated.js │ │ └── serverHelper.js │ ├── setupTests.js │ ├── App.test.js │ ├── index.css │ ├── components │ │ ├── ProtectedRoute.js │ │ ├── OkayMerchant.js │ │ ├── PasswordInput.js │ │ ├── TextInput.js │ │ └── Upload.js │ ├── reportWebVitals.js │ ├── index.js │ ├── App.js │ ├── logo.svg │ └── routes │ │ ├── Home.js │ │ ├── Login.js │ │ ├── SignUp.js │ │ ├── Dashboard.js │ │ └── Ecommerce.js ├── public │ ├── favicon.ico │ ├── favicon.png │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── .gitignore ├── app.YAML ├── tailwind.config.js ├── package.json └── README.md ├── .DS_Store ├── server ├── images │ ├── csvExample.png │ ├── Pinpoint_workflow.png │ └── Pinpoint_stack_architecture.png ├── .gitignore ├── models │ ├── Quote.js │ ├── Merchant.js │ └── Product.js ├── .gcloudignore ├── app.YAML ├── package.json ├── routes │ ├── merchant_routes.js │ ├── quote_routes.js │ ├── product_routes.js │ └── pincode_routes.js ├── server.js └── index.html ├── README.md └── LICENSE /client/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/containers/LoggedInContainer.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/.DS_Store -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/public/favicon.png -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /server/images/csvExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/server/images/csvExample.png -------------------------------------------------------------------------------- /client/src/assets/images/Name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/Name.png -------------------------------------------------------------------------------- /client/src/assets/images/drone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/drone.jpg -------------------------------------------------------------------------------- /client/src/assets/images/store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/store.png -------------------------------------------------------------------------------- /client/src/assets/images/watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/watch.png -------------------------------------------------------------------------------- /server/images/Pinpoint_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/server/images/Pinpoint_workflow.png -------------------------------------------------------------------------------- /client/src/assets/images/logoBig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/logoBig.png -------------------------------------------------------------------------------- /client/src/assets/images/Logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/Logo_black.png -------------------------------------------------------------------------------- /client/src/assets/images/Sparkthon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/Sparkthon.png -------------------------------------------------------------------------------- /client/src/assets/images/uploadIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/uploadIcon.png -------------------------------------------------------------------------------- /client/src/assets/images/watchBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/watchBlack.png -------------------------------------------------------------------------------- /client/src/assets/images/watchBlue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/watchBlue.png -------------------------------------------------------------------------------- /client/src/assets/images/watchGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/watchGreen.png -------------------------------------------------------------------------------- /client/src/assets/images/watchsmol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/client/src/assets/images/watchsmol.png -------------------------------------------------------------------------------- /server/images/Pinpoint_stack_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJcodess/Pinpoint/HEAD/server/images/Pinpoint_stack_architecture.png -------------------------------------------------------------------------------- /client/src/utils/config.js: -------------------------------------------------------------------------------- 1 | // URL of hosted API service. 2 | export const backendUrl = 'https://api-dot-pinpointgcp.df.r.appspot.com/api'; 3 | // URL for local API testing. 4 | // export const backendUrl = 'http://localhost:8000/api'; -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # production 8 | /build 9 | 10 | # misc 11 | .DS_Store 12 | .env 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/utils/isAuthenticated.js: -------------------------------------------------------------------------------- 1 | // src/utils/isAuthenticated.js 2 | import Cookies from 'js-cookie'; 3 | 4 | const isAuthenticated = () => { 5 | const token = Cookies.get('user'); 6 | return !!token; // Returns true if token exists, false otherwise 7 | }; 8 | 9 | export default isAuthenticated; 10 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url('https://fonts.googleapis.com/css2?family=Lato:wght@100;300;400;700;900&display=swap'); 6 | 7 | body { 8 | font-family: 'Lato', sans-serif; 9 | } 10 | 11 | 12 | .placeholder-dodgeblue::placeholder { 13 | color: #4285F4; /* Dodgeblue color */ 14 | } -------------------------------------------------------------------------------- /client/src/components/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | // src/components/ProtectedRoute.js 2 | import React from 'react'; 3 | import { Navigate, Outlet } from 'react-router-dom'; 4 | import isAuthenticated from '../utils/isAuthenticated'; 5 | 6 | const ProtectedRoute = () => { 7 | return isAuthenticated() ? : ; 8 | }; 9 | 10 | export default ProtectedRoute; 11 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /client/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /client/app.YAML: -------------------------------------------------------------------------------- 1 | runtime: nodejs20 2 | service: default 3 | 4 | handlers: 5 | # Serve static files under /static URL path 6 | - url: /static 7 | static_dir: build/static 8 | 9 | # Serve static files with specific extensions 10 | - url: /(.*\.(json|ico|js))$ 11 | static_files: build/\1 12 | upload: build/.*\.(json|ico|js)$ 13 | 14 | # Serve index.html for all other URLs 15 | - url: .* 16 | static_files: build/index.html 17 | upload: build/index.html 18 | 19 | -------------------------------------------------------------------------------- /client/src/components/OkayMerchant.js: -------------------------------------------------------------------------------- 1 | const OkayMerchant = ({ merchant }) => { 2 | return ( 3 |
4 |
5 |

{merchant.merchantName}

6 |

₹{merchant.sellingPrice}

7 |
8 |
9 | ); 10 | }; 11 | 12 | export default OkayMerchant; 13 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | ); 11 | 12 | // If you want to start measuring performance in your app, pass a function 13 | // to log results (for example: reportWebVitals(console.log)) 14 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 15 | reportWebVitals(); 16 | -------------------------------------------------------------------------------- /server/models/Quote.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const quoteSchema = new mongoose.Schema( 4 | { 5 | productId: { type: String, required: true }, 6 | merchantId: { type: String, required: true }, 7 | sellingPrice: { type: Number, required: true }, 8 | }, 9 | { 10 | collection: 'Quote', 11 | strict: true, 12 | timestamps: true, 13 | } 14 | ); 15 | 16 | // Compound index on unique (productId-merchantId). 17 | quoteSchema.index({ productId: 1, merchantId: 1 }, { unique: true }); 18 | 19 | module.exports = mongoose.model("Quote", quoteSchema); 20 | -------------------------------------------------------------------------------- /server/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /server/models/Merchant.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { v4: uuidv4 } = require('uuid'); 3 | 4 | const merchantSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | required: true, 9 | default: () => uuidv4(), 10 | }, 11 | merchantName: { type: String, required: true }, 12 | merchantAddress: { type: String, required: true }, 13 | merchantDescription: { type: String, required: true, default: "NA" }, 14 | }, 15 | { 16 | collection: 'Merchant', 17 | strict: true, 18 | timestamps: true, 19 | } 20 | ); 21 | 22 | module.exports = mongoose.model("Merchant", merchantSchema); 23 | -------------------------------------------------------------------------------- /server/app.YAML: -------------------------------------------------------------------------------- 1 | # Copyright 2018, Google, Inc. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # [START app_yaml] 15 | runtime: nodejs20 16 | service: api 17 | # [END app_yaml] -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "devStart": "nodemon server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "body-parser": "^1.20.2", 15 | "bson": "^6.3.0", 16 | "cors": "^2.8.5", 17 | "csv-parser": "^3.0.0", 18 | "dotenv": "^16.4.1", 19 | "express": "^4.18.2", 20 | "fs": "^0.0.1-security", 21 | "mongodb": "^6.3.0", 22 | "mongoose": "^8.1.1", 23 | "multer": "^1.4.5-lts.1", 24 | "redis": "^3.1.2", 25 | "uuid": "^9.0.1" 26 | }, 27 | "devDependencies": { 28 | "nodemon": "^3.0.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/src/components/PasswordInput.js: -------------------------------------------------------------------------------- 1 | const TextInput = ({ label, placeholder, value, setValue }) => { 2 | return ( 3 |
4 | 7 | { 14 | // setValue(e.target.value); 15 | // }} 16 | /> 17 |
18 | ); 19 | }; 20 | 21 | export default TextInput; -------------------------------------------------------------------------------- /server/models/Product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { v4: uuidv4 } = require('uuid'); 3 | 4 | const productSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | required: true, 9 | default: () => uuidv4(), 10 | }, 11 | merchantIds: { type: [String], required: true, default: [] }, 12 | productName: { type: String, required: true }, 13 | imageLink: { type: String, required: true }, 14 | productDescription: { type: String }, 15 | }, 16 | { 17 | collection: 'Product', 18 | strict: true, 19 | timestamps: true, 20 | } 21 | ); 22 | 23 | // Removed 'productCode' in favour of _id as the only primary index. 24 | // productSchema.index({ productCode: 1 }, { unique: false }); 25 | 26 | module.exports = mongoose.model("Product", productSchema); 27 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | fontFamily: { 10 | poppins: ["Poppins", "sans-serif"] 11 | }, 12 | height: { 13 | "1/10": "10%", 14 | "9/10": "90%" 15 | }, 16 | backgroundColor: { 17 | "app-black": "#121212", 18 | }, 19 | colors: { 20 | lightgray: "#F1F3F4", 21 | dodgeblue: "#4285F4", 22 | darkgray: "#54585B", 23 | lightgreen: "#C9E6DA", 24 | darkgreen: "#0F9D58", 25 | gray1: "#F6F6F6", 26 | gray2: "#BDBDBD", 27 | glass: "#7eafeb", 28 | deepblue: "#00bfff", 29 | brown: "#632F1E", 30 | } 31 | }, 32 | }, 33 | variants: { 34 | extend: {}, 35 | }, 36 | plugins: [], 37 | } 38 | 39 | -------------------------------------------------------------------------------- /client/src/components/TextInput.js: -------------------------------------------------------------------------------- 1 | const TextInput = ({ 2 | label, 3 | placeholder, 4 | className, 5 | value, 6 | setValue, 7 | labelClassName, 8 | type, 9 | }) => { 10 | return ( 11 |
14 | 17 | 18 | 22 |
23 | ); 24 | }; 25 | 26 | export default TextInput; 27 | 28 | // className="text-xl p-3 border border-solid border-dodgeblue bg-glass rounded placeholder-dodgeblue" -------------------------------------------------------------------------------- /client/src/utils/serverHelper.js: -------------------------------------------------------------------------------- 1 | import { backendUrl } from './config'; 2 | 3 | export const makePOSTRequest = async (route, body) => { 4 | const response = await fetch(backendUrl + route, { 5 | method: 'POST', 6 | body: body, // Pass FormData directly as body 7 | }); 8 | 9 | // const formattedResponse = await response.json(); 10 | // return formattedResponse; 11 | return response; 12 | }; 13 | 14 | export const makeDELETERequest = async (route, body) => { 15 | const response = await fetch(backendUrl + route, { 16 | method: 'DELETE', 17 | body: body, 18 | }); 19 | return response; 20 | }; 21 | 22 | export const makeGETRequest = async (route, body) => { 23 | const response = await fetch(backendUrl + route, { 24 | method: 'GET', 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | body: JSON.stringify(body), 29 | }); 30 | // console.log("response", response); 31 | const formattedResponse = await response.json(); 32 | return formattedResponse; 33 | }; 34 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@iconify-icons/fa": "^1.2.4", 7 | "@react-three/drei": "^9.80.3", 8 | "@react-three/fiber": "^8.13.6", 9 | "@testing-library/jest-dom": "^5.17.0", 10 | "@testing-library/react": "^13.4.0", 11 | "@testing-library/user-event": "^13.5.0", 12 | "js-cookie": "^3.0.5", 13 | "react": "^18.2.0", 14 | "react-cookie": "^7.1.4", 15 | "react-dom": "^18.2.0", 16 | "react-router-dom": "^6.15.0", 17 | "react-scripts": "5.0.1", 18 | "three": "^0.155.0", 19 | "vercel": "^31.2.3", 20 | "web-vitals": "^2.1.4" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@iconify/react": "^4.1.1", 48 | "tailwindcss": "^3.3.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import logo from './logo.svg'; 2 | import './App.css'; 3 | import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; 4 | import SignUp from './routes/SignUp.js'; 5 | import Login from './routes/Login.js'; 6 | import Home from './routes/Home.js'; 7 | import Ecommerce from './routes/Ecommerce'; 8 | import Dashboard from './routes/Dashboard'; 9 | import { useCookies } from 'react-cookie'; 10 | import ProtectedRoute from './components/ProtectedRoute'; 11 | 12 | function App() { 13 | 14 | // const [cookie] = useCookies(['token']); 15 | const isUserLoggedIn = !!localStorage.getItem('user') 16 | return ( 17 |
18 | 19 | 20 | 23 | /> 24 | 27 | /> 28 | 31 | /> 32 | 35 | /> 36 | }> 37 | } /> 38 | 39 | 40 | 41 |
42 | ); 43 | } 44 | 45 | export default App; 46 | 47 | -------------------------------------------------------------------------------- /client/src/components/Upload.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import uploadIcon from '../assets/images/uploadIcon.png'; 3 | // import { Icon } from '@iconify/react'; 4 | 5 | // const Upload = () => { 6 | // return ( 7 | //
8 | // 26 | //
27 | // ); 28 | // }; 29 | 30 | // export default Upload; 31 | -------------------------------------------------------------------------------- /server/routes/merchant_routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const Merchant = require("../models/Merchant"); 3 | const router = express.Router(); 4 | 5 | // Getting a list of all merchants. 6 | router.get("/merchants", async (req, res) => { 7 | try { 8 | const products = await Merchant.find(); 9 | res.json(products); 10 | } catch (err) { 11 | console.error(err); 12 | res.status(500).json({ error: "Internal Server Error" }); 13 | } 14 | }); 15 | 16 | // Getting specific merchant by its ID. 17 | router.get("/merchants/:id", async (req, res) => { 18 | try { 19 | const merchant = await Merchant.findById(req.params.id); 20 | if (!merchant) { 21 | return res.status(404).json({ error: "merchant not found" }); 22 | } 23 | res.json(merchant); 24 | } catch (err) { 25 | console.error(err); 26 | res.status(500).json({ error: "Internal Server Error" }); 27 | } 28 | }); 29 | 30 | // Creating a new merchant. 31 | router.post("/create", async (req, res) => { 32 | try { 33 | const { merchantName, merchantAddress, merchantDescription } = req.body; 34 | const newMerchant = new Merchant({ 35 | merchantName, 36 | merchantAddress, 37 | merchantDescription, 38 | }); 39 | const savedMerchant = await newMerchant.save(); 40 | res.status(201).json(savedMerchant); 41 | } catch (err) { 42 | console.error(err); 43 | res.status(500).json({ error: "Internal Server Error" }); 44 | } 45 | }); 46 | 47 | module.exports = router; 48 | -------------------------------------------------------------------------------- /server/routes/quote_routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const Quote = require("../models/Quote"); 3 | const router = express.Router(); 4 | 5 | // Route to get a quote for a merchant and product. 6 | // Use the GET with "/api/quote/getQuote?p=productId&m=merchantId". 7 | router.get("/getQuote", async (req, res) => { 8 | try { 9 | // Extracting query parameters. 10 | const productId = req.query.p; 11 | const merchantId = req.query.m; 12 | 13 | // Validation. 14 | if (!productId || !merchantId) { 15 | return res.status(400).json({ error: "Invalid input data" }); 16 | } 17 | 18 | // Find the quote based on merchantId and productId 19 | const quote = await Quote.findOne({ productId, merchantId }); 20 | 21 | if (!quote) { 22 | return res.status(404).json({ error: "Quote not found" }); 23 | } 24 | res.status(200).json(quote); 25 | } catch (err) { 26 | console.error(err); 27 | res.status(500).json({ error: "Internal Server Error" }); 28 | } 29 | }); 30 | 31 | // Route to create a new quote for a merchant. 32 | router.post("/create/:merchantId", async (req, res) => { 33 | try { 34 | const { merchantId } = req.params; 35 | const { productId, sellingPrice } = req.body; 36 | 37 | // Validation. 38 | if (!productId || !sellingPrice || isNaN(sellingPrice)) { 39 | return res.status(400).json({ error: "Invalid request format." }); 40 | } 41 | 42 | const newQuote = new Quote({ 43 | productId, 44 | merchantId, 45 | sellingPrice, 46 | }); 47 | const savedQuote = await newQuote.save(); 48 | res.status(201).json(savedQuote); 49 | } catch (err) { 50 | console.error(err); 51 | res.status(500).json({ error: "Internal Server Error" }); 52 | } 53 | }); 54 | 55 | module.exports = router; 56 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | PinPoint 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const mongoose = require("mongoose"); 3 | const envconfig = require("dotenv").config(); 4 | const productRoutes = require("./routes/product_routes"); 5 | const merchantRoutes = require("./routes/merchant_routes"); 6 | const pincodeRoutes = require("./routes/pincode_routes"); 7 | const redis = require('redis'); 8 | const quoteRoutes = require("./routes/quote_routes"); 9 | const cors = require('cors'); 10 | 11 | const app = express(); 12 | const port = process.env.PORT || 8000; 13 | 14 | const CONNECTION_MONGODB_URI = 15 | process.env.MONGODB_URI || "mongodb://localhost/testdb"; 16 | 17 | mongoose.connect(CONNECTION_MONGODB_URI, { 18 | useNewUrlParser: true, 19 | useUnifiedTopology: true, 20 | }); 21 | 22 | const db = mongoose.connection; 23 | 24 | // Opening MongoDB connection. 25 | db.on("error", console.error.bind(console, "MongoDB connection error:")); 26 | db.once("open", () => { 27 | console.log("Connected to MongoDB"); 28 | }); 29 | 30 | // For CORS. 31 | app.use(cors()); 32 | 33 | // For parsing JSON in req.body. 34 | app.use(express.json()); 35 | 36 | // Home route. 37 | app.get("/", (req, res) => { 38 | res.sendFile(__dirname + '/index.html'); 39 | }); 40 | 41 | // Mounting remaining routes. 42 | app.use("/api/product", productRoutes); 43 | app.use("/api/merchant", merchantRoutes); 44 | app.use("/api/pincode", pincodeRoutes); 45 | app.use("/api/quote", quoteRoutes); 46 | 47 | // Function to close connection. 48 | const closeMongoDBConnection = () => { 49 | mongoose.connection 50 | .close() 51 | .then(() => { 52 | console.log("Closing MongoDB connection due to app termination."); 53 | process.exit(0); 54 | }) 55 | .catch((err) => { 56 | console.error("Error closing MongoDB connection:", err); 57 | process.exit(1); 58 | }); 59 | }; 60 | 61 | process.on("SIGINT", () => { 62 | closeMongoDBConnection(); 63 | }); 64 | process.on("SIGTERM", () => { 65 | closeMongoDBConnection(); 66 | }); 67 | 68 | app.listen(port, () => { 69 | console.log(`Server is running on http://localhost:${port}`); 70 | }); 71 | -------------------------------------------------------------------------------- /client/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/routes/product_routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const Product = require("../models/Product"); 3 | const Merchant = require("../models/Merchant"); 4 | const router = express.Router(); 5 | 6 | // Getting a list of all products. 7 | router.get("/items", async (req, res) => { 8 | try { 9 | const products = await Product.find(); 10 | res.json(products); 11 | } catch (err) { 12 | console.error(err); 13 | res.status(500).json({ error: "Internal Server Error" }); 14 | } 15 | }); 16 | 17 | // Getting specific product by its ID. 18 | router.get("/items/:id", async (req, res) => { 19 | try { 20 | const product = await Product.findById(req.params.id); 21 | if (!product) { 22 | return res.status(404).json({ error: "Product not found" }); 23 | } 24 | res.json(product); 25 | } catch (err) { 26 | console.error(err); 27 | res.status(500).json({ error: "Internal Server Error" }); 28 | } 29 | }); 30 | 31 | // Creating a new product. 32 | router.post("/create", async (req, res) => { 33 | try { 34 | const { productName, imageLink, productDescription } = req.body; 35 | const newProduct = new Product({ 36 | productName, 37 | imageLink, 38 | productDescription, 39 | }); 40 | const savedProduct = await newProduct.save(); 41 | res.status(201).json(savedProduct); 42 | } catch (err) { 43 | console.error(err); 44 | res.status(500).json({ error: "Internal Server Error" }); 45 | } 46 | }); 47 | 48 | // Appending a list of merchantId to the product. 49 | router.post("/insert/:id", async (req, res) => { 50 | try { 51 | const { merchantsArr } = req.body; 52 | const currProduct = await Product.findById(req.params.id); 53 | if (!currProduct) { 54 | return res.status(404).json({ error: "Product not found" }); 55 | } 56 | // Validating each merchantId before inserting. 57 | for (const merchantId of merchantsArr) { 58 | // Check if the merchantId exists in the database 59 | const merchantExists = await Merchant.exists({ _id: merchantId }); 60 | if (!merchantExists) { 61 | return res 62 | .status(400) 63 | .json({ error: `Merchant with ID ${merchantId} not found` }); 64 | } 65 | } 66 | // Concatenating merchantsArr to existing merchantIds. 67 | currProduct.merchantIds = currProduct.merchantIds.concat(merchantsArr); 68 | const savedProduct = await currProduct.save(); 69 | res.status(200).json(savedProduct); 70 | } catch (err) { 71 | console.error(err); 72 | res.status(500).json({ error: "Internal Server Error" }); 73 | } 74 | }); 75 | 76 | module.exports = router; 77 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /client/src/routes/Home.js: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import Cookies from 'js-cookie'; 3 | import { Link } from 'react-router-dom'; 4 | import { Icon } from '@iconify/react'; 5 | import PinPointName from '../assets/images/pinpointName.svg'; 6 | import ONDC from '../assets/images/ondc.svg'; 7 | import StartupIndia from '../assets/images/startupindia.svg'; 8 | import Decor from '../assets/images/decor.svg'; 9 | import HomeImg from '../assets/images/homeimg.svg'; 10 | 11 | const Home = () => { 12 | const navigate = useNavigate(); 13 | 14 | const handleGetStarted = () => { 15 | const user = Cookies.get('user'); 16 | if (user) { 17 | navigate('/login'); 18 | } else { 19 | navigate('/signup'); 20 | } 21 | }; 22 | 23 | return ( 24 |
25 | {/* left side */} 26 |
27 |
28 | pinpoint 29 |
30 |
31 | Build for Bharat 32 |
33 |
34 |

supported by

35 | startup India 36 |
37 |
38 | 39 |

Retail & Logistics

40 |
41 |
42 |

Optimal storage & retrieval in

43 |

M*N sparse matrix

44 |
45 |
46 | 50 |
51 |
52 | {/* right side */} 53 |
54 |
55 | 56 | pinpoint 57 |

click here to view buyer app

58 | 59 |
60 |
61 | decor 62 |
63 |
64 | home img 65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | export default Home; 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PinPoint 2 | 3 | ## Optimal Storage and Retrieval for Pincode Serviceability 4 | 5 | Deployed on Google Cloud — [_Website Link_](https://pinpointgcp.df.r.appspot.com) 6 | 7 | **PinPoint** is an application developed to solve the problem of pincode serviceability for products on the [ONDC](https://ondc.org/) website. 8 | 9 | Using the _seller apps_, merchants are able to define the products & services they can deliver, as well as the pincodes to which they can deliver them. 10 | 11 | On the side of _buyer app_, to check if a product can be delivered to their location by any of the merchants, the user enters their pincode. 12 | Considering there are more than _30K_ pincodes and at least _100 million_ merchants (of which about _10%_ may enable pincode based serviceability), we want the verification to be real-time. 13 | 14 | ## Our Solution 15 | 16 | Our solution for this problem can be summarized as follows: 17 | 18 | - For storing data at-scale, we use a document-based NoSQL Database ([MongoDB](https://www.mongodb.com/), hosted at Google Cloud) as our `primary DB`. This is used to store the majority of data relating to products, merchants, price quotes, etc. 19 | - For making fast queries, and accessing frequently-used data, we store a mapping of "merchant-ID" to "set of serviceable pincodes" in ([Redis](https://redis.io/), hosted at Google Cloud), as our `secondary DB` or in-memory storage. This is used to check if a particular merchant services the given user pincode. 20 | - We create a simple [API](https://api-dot-pinpointgcp.df.r.appspot.com) using [Express](https://expressjs.com/) to serve the data and interact with our 2 databases using `POST` and `GET` methods. 21 | - When a user opens the e-commerce page for any product, the product details are fetched from primary DB and stored in client-side, which includes the list of merchant-IDs, for each merchant that delivers the product. 22 | - Once the user enters their pincode to check the serviceability in their region, the list of merchant-IDs is traversed, and the in-memory `secondary DB` is queried to see if the merchant delivers to that pincode in constant `O(1)` time, concurrently for each merchant. 23 | - If there is a pincode match, the merchant details are displayed to the user along with the price quoted by them, by making a final query to `primary DB`, where appropriate indexes have been created to make the queries even faster. 24 | - The system is designed to work well with scale, as more merchants are added in the ONDC database. 25 | 26 | ![Pinpoint Realtime Query Workflow](/server/images/Pinpoint_workflow.png) 27 | 28 | ## Optimizations 29 | 30 | 1. We have used Redis (or any main memory caching mechanism) for making the real-time pincode query fast, as Redis set offers `O(1) set lookup`. 31 | 32 | 2. To fetch product details quickly, we pull the set of merchantIds that deliver the product on page-click, instead of doing it during the pincode query. The `productId` value has been indexed, to further decrease the query time on page component load. 33 | 34 | 3. For each *real-time pincode query* by user, the membership of the pincode in the Redis set is checked for each merchantId `concurrently`, which significantly reduces latency, and is designed to scale well. 35 | 36 | 4. `Indexing` is used to further reduce the time for displaying serviceable merchant details. Here, *merchantId* is indexed for the merchant details, whereas a combination of *merchantId-productId* has been indexed, to fetch and display the quoted pricing. 37 | 38 | All these optimizations have been made to improve the user experience. 39 | 40 | ![Pinpoint Stack Architecture](/server/images/Pinpoint_stack_architecture.png) 41 | 42 | ## Demo 43 | 44 | Demo Video on YouTube: 45 | 46 | [![Watch the video](https://img.youtube.com/vi/5ZxIyJHNei8/0.jpg)](https://www.youtube.com/watch?v=SRinIlxBpC0&ab_channel=DhruvJain) 47 | 48 | ## API Reference 49 | 50 | [API Documentation](https://api-dot-pinpointgcp.df.r.appspot.com) — Deployed on Google Cloud 51 | 52 | [Raw Link](/server/index.html) 53 | 54 | ## Steps to run locally 55 | 56 | 1. Clone the repository: 57 | ``` 58 | git clone https://github.com/DJcodess/Pinpoint.git 59 | ``` 60 | 61 | 2. Install the required dependencies for both `/server` and `/client` individually. 62 | ``` 63 | cd server 64 | npm install 65 | cd ../client 66 | npm install 67 | ``` 68 | 69 | 3. Configure the environment variables. Start the Express server by running `/server/server.js` in Node. 70 | 71 | 4. Run the React development server in `/client`. 72 | ``` 73 | npm start 74 | ``` 75 | 76 | ## Environment Variables 77 | 78 | To run this project, you will need to add the following environment variables to your .env file 79 | 80 | `MONGODB_URI` 81 | 82 | `REDIS_HOST` 83 | 84 | `REDIS_PORT` 85 | 86 | `REDIS_USER` 87 | 88 | `REDIS_PASSWORD` 89 | 90 | ## Authors 91 | 92 | - [@DJcodess](https://github.com/DJcodess) 93 | - [@Vasu1712](https://github.com/Vasu1712) 94 | - [@abhishekghosh-in](https://github.com/abhishekghosh-in) 95 | - [@abhinavprakash-2000](https://github.com/abhinavprakash-2000) 96 | 97 | This project was developed towards our submission in the Build for Bharat [Hackathon](https://hack2skill.com/build-for-bharat-hackathon-ondc-google-cloud) 98 | -------------------------------------------------------------------------------- /client/src/routes/Login.js: -------------------------------------------------------------------------------- 1 | import store from '../assets/images/store.png'; 2 | import { Link, useNavigate } from "react-router-dom"; 3 | import { useState } from 'react'; 4 | import Cookies from 'js-cookie'; 5 | import PinPointName from '../assets/images/pinpointName.svg'; 6 | import ONDC from '../assets/images/ondc.svg'; 7 | 8 | const Login = () => { 9 | const navigate = useNavigate(); 10 | const [email, setEmail] = useState(""); 11 | const [password, setPassword] = useState(""); 12 | const [emailError, setEmailError] = useState(""); 13 | const [passwordError, setPasswordError] = useState(""); 14 | 15 | const validateEmail = (email) => { 16 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 17 | return emailRegex.test(email); 18 | }; 19 | 20 | const handleLogin = () => { 21 | setEmailError(""); 22 | setPasswordError(""); 23 | 24 | if (!validateEmail(email)) { 25 | setEmailError("Please enter a valid email address!"); 26 | return; 27 | } 28 | if (!password || password.length === 0) { 29 | setPasswordError("Please enter your password"); 30 | return; 31 | } 32 | 33 | if (!Cookies.get('user')) { 34 | alert("You need to register first!"); 35 | return; 36 | } 37 | const user = JSON.parse(Cookies.get('user')); 38 | if (user && user.email === email && user.password === password) { 39 | navigate('/dashboard'); 40 | } else { 41 | // Handle login error 42 | alert("Invalid Log-in credentials!!"); 43 | } 44 | }; 45 | 46 | return ( 47 |
48 | {/* left side */} 49 |
50 |
51 | pinpoint 52 |
53 |
54 | store 55 |
56 |
57 | {/* right side */} 58 |
59 |
60 | {/* form */} 61 |
62 | 63 |
64 | PinPoint 65 |
66 | 67 | { 73 | setEmail(e.target.value); 74 | setEmailError(""); 75 | }} 76 | /> 77 | {emailError &&

{emailError}

} 78 | 79 | { 85 | setPassword(e.target.value); 86 | setPasswordError(""); 87 | }} 88 | /> 89 | {passwordError &&

{passwordError}

} 90 | 91 | 97 |
98 |

Or, new here?

99 | 100 | Register 101 | 102 |
103 |
104 |
105 |
106 |
107 | ); 108 | }; 109 | 110 | export default Login; 111 | -------------------------------------------------------------------------------- /server/routes/pincode_routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const redis = require("redis"); 3 | const multer = require("multer"); 4 | const csvParser = require("csv-parser"); 5 | const fs = require("fs"); 6 | 7 | const upload = multer({ dest: "/tmp" }); // Destination folder for uploaded files 8 | const router = express.Router(); 9 | const client = redis.createClient({ 10 | host: process.env.REDIS_HOST, 11 | port: process.env.REDIS_PORT, 12 | username: process.env.REDIS_USER, 13 | password: process.env.REDIS_PASSWORD, 14 | }); 15 | 16 | client.on("error", (err) => { 17 | console.error("Redis error:", err); 18 | }); 19 | 20 | // API endpoint to check if a pincode is serviced by a merchant. 21 | router.get("/:merchantId/:pincode", (req, res) => { 22 | const { merchantId, pincode } = req.params; 23 | 24 | // Check if the pincode exists in the set associated with the specified merchant ID 25 | client.sismember(`merchant:${merchantId}`, pincode, (err, reply) => { 26 | if (err) { 27 | console.error("Error checking pincode:", err); 28 | res.status(500).send("Error checking pincode"); 29 | } else { 30 | if (reply === 1) { 31 | res.status(200).json({ serviced: true }); 32 | } else { 33 | res.status(200).json({ serviced: false }); 34 | } 35 | } 36 | }); 37 | }); 38 | 39 | // API endpoint to get a list of all pincodes serviced by the merchant. 40 | router.get("/:merchantId", (req, res) => { 41 | const merchantId = req.params.merchantId; 42 | client.smembers(`merchant:${merchantId}`, (err, reply) => { 43 | if (err) { 44 | console.error("Error fetching list of pincodes: ", err); 45 | res.status(500).send("Error fetching pincodes"); 46 | } else { 47 | reply.sort(); 48 | // console.log("reply: ", reply); 49 | res.status(200).json({pincodesList: reply}); 50 | } 51 | }); 52 | }); 53 | 54 | // POST endpoint to upload CSV file and set pincodes for a merchant. 55 | router.post("/:merchantId", upload.single("csvFile"), (req, res) => { 56 | const merchantId = req.params.merchantId; 57 | const csvFilePath = req.file.path; 58 | 59 | fs.createReadStream(csvFilePath) 60 | .pipe(csvParser()) 61 | .on("data", (row) => { 62 | // Process each row from the CSV 63 | const pincodeObject = row; // Extract the pincode object from the row 64 | //selecting pincode as the values (pincode values) of the pincode object 65 | console.log("Setting pincode: ", pincodeObject); 66 | let pincode = pincodeObject["pincode"]; 67 | // Check if pincode is valid before adding to the set 68 | if (pincode) { 69 | client.sadd(`merchant:${merchantId}`, pincode, (err, reply) => { 70 | if (err) { 71 | console.error("Error adding pincode to set:", err); 72 | res.status(500).send("Error adding pincode to set"); 73 | } else { 74 | console.log( 75 | `Pincode ${pincode} added to set for merchant ${merchantId}` 76 | ); 77 | } 78 | }); 79 | } else { 80 | console.warn("Invalid pincode:", pincode); 81 | } 82 | }) 83 | .on("end", () => { 84 | console.log("CSV processing complete"); 85 | res 86 | .status(200) 87 | .send("CSV processing complete, added the pincodes successfully"); 88 | }) 89 | .on("error", (err) => { 90 | console.error("Error parsing CSV:", err); 91 | res.status(500).send("Error parsing CSV"); 92 | }); 93 | }); 94 | 95 | // POST endpoint to append a single pincode to the merchant serviceability area. 96 | router.post("/:merchantId/:pincode", (req, res) => { 97 | const merchantId = req.params.merchantId; 98 | const pincode = req.params.pincode; 99 | 100 | client.sadd(`merchant:${merchantId}`, pincode, (err, reply) => { 101 | if (err) { 102 | console.error("Error adding pincode to set:", err); 103 | res.status(500).send("Error adding pincode to set"); 104 | } else { 105 | console.log(`Pincode ${pincode} added to set for merchant ${merchantId}`); 106 | res 107 | .status(200) 108 | .send(`Pincode ${pincode} added to set for merchant ${merchantId}`); 109 | } 110 | }); 111 | }); 112 | 113 | // DELETE endpoint to remove a single pincode from the merchant serviceability area. 114 | router.delete("/remove/:merchantId/:pincode", (req, res) => { 115 | const merchantId = req.params.merchantId; 116 | const pincode = req.params.pincode; 117 | client.srem(`merchant:${merchantId}`, pincode, (err, reply) => { 118 | if (err) { 119 | console.error("Error removing pincode to set:", err); 120 | res.status(500).send("Error removing pincode from the set"); 121 | } else { 122 | console.log(`Pincode ${pincode} removed from set for merchant ${merchantId}`); 123 | res 124 | .status(200) 125 | .send(`Pincode ${pincode} removed from set for merchant ${merchantId}`); 126 | } 127 | }); 128 | }); 129 | 130 | // DELETE endpoint to clear the entire set of pincodes for merchant serviceability area. 131 | router.delete("/remove/:merchantId", (req, res) => { 132 | const merchantId = req.params.merchantId; 133 | console.log("api hit"); 134 | client.del(`merchant:${merchantId}`, (err, reply) => { 135 | if (err) { 136 | console.error("Error clearing the pincodes set:", err); 137 | res.status(500).send("Error clearing pincodes from the set"); 138 | } else { 139 | console.log(`All pincodes removed from set for merchant ${merchantId}`); 140 | res 141 | .status(200) 142 | .send(`All pincodes removed from set for merchant ${merchantId}`); 143 | } 144 | }); 145 | }); 146 | 147 | module.exports = router; 148 | -------------------------------------------------------------------------------- /client/src/routes/SignUp.js: -------------------------------------------------------------------------------- 1 | import store from '../assets/images/store.png'; 2 | import { Link, useNavigate } from "react-router-dom"; 3 | import PinPointName from '../assets/images/pinpointName.svg'; 4 | import ONDC from '../assets/images/ondc.svg'; 5 | import { useState } from 'react'; 6 | import Cookies from 'js-cookie'; 7 | 8 | const SignUp = () => { 9 | const navigate = useNavigate(); 10 | 11 | const [name, setName] = useState(""); 12 | const [address, setAddress] = useState(""); 13 | const [description, setDescription] = useState(""); 14 | const [email, setEmail] = useState(""); 15 | const [password, setPassword] = useState(""); 16 | 17 | const [nameError, setNameError] = useState(""); 18 | const [addressError, setAddressError] = useState(""); 19 | const [emailError, setEmailError] = useState(""); 20 | const [passwordError, setPasswordError] = useState(""); 21 | 22 | const validateEmail = (email) => { 23 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 24 | return emailRegex.test(email); 25 | }; 26 | 27 | const handleSignUp = () => { 28 | setNameError(""); 29 | setAddressError(""); 30 | setEmailError(""); 31 | setPasswordError(""); 32 | 33 | if (!name) { 34 | setNameError("Please enter your name"); 35 | return; 36 | } 37 | if (!/^[a-zA-Z].*$/.test(name)) { 38 | setNameError("Name must start with a letter"); 39 | return; 40 | } 41 | if (!address) { 42 | setAddressError("Please enter your address"); 43 | return; 44 | } 45 | if (!validateEmail(email)) { 46 | setEmailError("Please enter a valid email address"); 47 | return; 48 | } 49 | 50 | if (!password) { 51 | setPasswordError("Please enter your password"); 52 | return; 53 | } 54 | 55 | // TODO: Implement sign up logic here (token). 56 | // For simplicity, we save the user data to a cookie 57 | const user = { name, address, description, email, password }; 58 | Cookies.set('user', JSON.stringify(user), { expires: 7 }); 59 | navigate('/login'); 60 | }; 61 | 62 | return ( 63 |
64 | {/* left side */} 65 |
66 |
67 | pinpoint 68 |
69 |
70 | store 71 |
72 |
73 | {/* right side */} 74 |
75 |
76 | {/* form */} 77 |
78 | 79 |
80 | PinPoint 81 |
82 | 83 | { 89 | setName(e.target.value); 90 | setNameError(""); 91 | }} 92 | /> 93 | {nameError &&

{nameError}

} 94 | 95 | { 101 | setAddress(e.target.value); 102 | setAddressError(""); 103 | }} 104 | /> 105 | {addressError &&

{addressError}

} 106 | 107 | { 113 | setDescription(e.target.value); 114 | }} 115 | /> 116 | 117 | { 123 | setEmail(e.target.value); 124 | setEmailError(""); 125 | }} 126 | /> 127 | {emailError &&

{emailError}

} 128 | 129 | { 135 | setPassword(e.target.value); 136 | setPasswordError(""); 137 | }} 138 | /> 139 | {passwordError &&

{passwordError}

} 140 | 141 | 147 |
148 |

Already a merchant,

149 | 150 | Sign In 151 | 152 |
153 |
154 |
155 |
156 |
157 | ); 158 | }; 159 | 160 | export default SignUp; 161 | -------------------------------------------------------------------------------- /client/src/routes/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Icon } from '@iconify/react'; 3 | import PinPointName from '../assets/images/pinpointName.svg'; 4 | import { makeGETRequest, makePOSTRequest, makeDELETERequest } from '../utils/serverHelper'; 5 | 6 | const Dashboard = () => { 7 | const [merchantId, setMerchantId] = useState("51ce09c7-17f2-4e7b-97e7-1a63e800d543"); 8 | const [merchantName, setMerchantName] = useState(""); 9 | const [pincodes, setPincodes] = useState([]); 10 | const [csvFile, setCsvFile] = useState(""); 11 | 12 | useEffect(() => { 13 | setDetailsOnPage(); 14 | }, [merchantId]); 15 | 16 | useEffect(() => { 17 | setDetailsOnPage(); 18 | setMerchantId("51ce09c7-17f2-4e7b-97e7-1a63e800d543"); 19 | }, []); 20 | 21 | const setDetailsOnPage = async () => { 22 | try { 23 | let retObject = await makeGETRequest(`/merchant/merchants/${merchantId}`); 24 | setMerchantName(retObject["merchantName"]); 25 | await updatePincodesList(); 26 | } catch (err) { 27 | console.log("Error in getting merchant details ", err); 28 | } 29 | }; 30 | 31 | const updatePincodesList = async () => { 32 | try { 33 | let retObject = await makeGETRequest(`/pincode/${merchantId}`); 34 | const pincodesList = retObject["pincodesList"]; 35 | console.log("Received pincodes list successfully: ", pincodesList); 36 | setPincodes(pincodesList); 37 | } catch (err) { 38 | console.log("Error in fetching pincodes list ", err); 39 | } 40 | } 41 | 42 | const handleSingleInsert = async () => { 43 | const pincodeInput = document.getElementById("single-pincode"); 44 | const pincode = pincodeInput.value; 45 | if (!/^\d{6}$/.test(pincode)) { 46 | console.log("Invalid pincode format, you entered: ", pincode); 47 | alert("Invalid Request! Pincode has to be a 6-digit number only."); 48 | return; 49 | } 50 | try { 51 | await makePOSTRequest(`/pincode/${merchantId}/${pincode}`); 52 | updatePincodesList(); 53 | document.getElementById("single-pincode").value = ""; 54 | } catch (err) { 55 | console.log("Error in appending pincode to merchant's list: ", err); 56 | } 57 | } 58 | 59 | const handleCsvMassInsert = async () => { 60 | if (!csvFile) { 61 | alert('Please upload a CSV file before submitting the form.'); 62 | return; 63 | } 64 | 65 | const formData = new FormData(); 66 | formData.append("csvFile", csvFile); 67 | 68 | try { 69 | const response = await makePOSTRequest(`/pincode/${merchantId}`, formData); 70 | setCsvFile(""); 71 | updatePincodesList(); 72 | console.log(response); 73 | alert('Pincodes inserted successfully!'); 74 | } catch (error) { 75 | console.error('Error:', error); 76 | alert('An error occurred while inserting pincodes. Please try again later.'); 77 | } 78 | } 79 | 80 | const handleMassDelete = async () => { 81 | try { 82 | const response = await makeDELETERequest(`/pincode/remove/${merchantId}`); 83 | updatePincodesList(); 84 | console.log(response); 85 | alert('All pincodes deleted successfully!'); 86 | } catch (error) { 87 | console.error('Error:', error); 88 | alert('An error occurred while deleting pincodes.'); 89 | } 90 | } 91 | 92 | const handleSingleDelete = async (pincode) => { 93 | try { 94 | const response = await makeDELETERequest(`/pincode/remove/${merchantId}/${pincode}`); 95 | updatePincodesList(); 96 | console.log(response); 97 | alert('Selected pincode deleted successfully!'); 98 | } catch (error) { 99 | console.error('Error:', error); 100 | alert('An error occurred while deleting pincodes.'); 101 | } 102 | } 103 | 104 | return ( 105 |
106 |
107 | pinpoint 108 |

Dashboard

109 |
110 |
111 | Hello! {merchantName} 112 |
113 |
114 |
115 |

Enter Pincodes manually

116 |
117 | 124 | 127 |
128 |
129 |
130 |
131 | 132 |
133 |

Import CSV

134 |

csv file

135 |
136 |
137 |
138 |
139 | {!csvFile && ( 140 | 151 | )} 152 | {csvFile && ( 153 |
154 |

File attached: {csvFile.name}

155 | setCsvFile("")} /> 157 |
158 | )} 159 |
160 |
161 | 164 |
165 |
166 |
167 |
168 |
169 |
170 |

Total Pincodes Serving ({pincodes.length})

171 | 172 |
173 |
174 | {pincodes.length === 0 ? ( 175 |

No PINCodes found

176 | ) : ( 177 |
    178 | {Array.isArray(pincodes) && pincodes.map((code, index) => ( 179 |
  • 180 |
    181 | 182 |
    183 |

    {code}

    184 |
    185 |
    186 |
    187 | handleSingleDelete(code)} /> 189 |
    190 |
  • 191 | ))} 192 |
193 | )} 194 |
195 |
196 |
197 |
198 | ); 199 | }; 200 | 201 | export default Dashboard; 202 | -------------------------------------------------------------------------------- /client/src/assets/images/decor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /client/src/routes/Ecommerce.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Icon } from '@iconify/react'; 3 | import Watch from '../assets/images/watch.png'; 4 | import Watchsmol from '../assets/images/watchsmol.png'; 5 | import WatchBlack from '../assets/images/watchBlack.png'; 6 | import WatchBlue from '../assets/images/watchBlue.png'; 7 | import WatchGreen from '../assets/images/watchGreen.png'; 8 | import ONDC from '../assets/images/ondc.svg'; 9 | import { makeGETRequest, makePOSTRequest } from '../utils/serverHelper'; 10 | import OkayMerchant from '../components/OkayMerchant.js' 11 | 12 | 13 | const Ecommerce = () => { 14 | const [pincode, setPincode] = useState(''); 15 | const [showUnderDeliverable, setShowUnderDeliverable] = useState(false); 16 | const [showUnderNonDeliverable, setShowUnderNonDeliverable] = useState(false); 17 | const [checked, setChecked] = useState(false); 18 | const [blurButtons, setBlurButtons] = useState(true); 19 | // productId of Sternglas zirkel. 20 | const [productId, setProductId] = useState("46b6c84b-93b1-40f8-86b5-196f7a7202d3"); 21 | const [merchIds, setMerchIds] = useState([]); 22 | // Merchants that service. 23 | const [okMerchants, setOkMerchants] = useState([]); 24 | 25 | 26 | // To store merchIds when the product page component renders (i.e. product is clicked). 27 | useEffect (() => { 28 | const setMerchIdsOnPageClick = async () => { 29 | try { 30 | const merchantIdArr = await returnMerchantIds(productId); 31 | 32 | 33 | console.log("Received Merchant IDs successfully: ", merchantIdArr); 34 | setMerchIds(merchantIdArr); 35 | } catch (err) { 36 | console.log("Error in getting merchantId data", err); 37 | } 38 | }; 39 | setMerchIdsOnPageClick() 40 | }, []); 41 | 42 | 43 | const handleCheck = async () => { 44 | setChecked(true); 45 | setOkMerchants([]); 46 | 47 | 48 | if (merchIds.length > 0) { 49 | // setShowUnderDeliverable(true); 50 | setBlurButtons(false); 51 | } else { 52 | console.log("No merchants sell this product"); 53 | 54 | return; 55 | } 56 | 57 | 58 | // Making O(1) constant time asynchronous queries to each Redis pincode set. 59 | const serviceableMerchantsPromises = merchIds.map( async (currMerchId) => { 60 | try { 61 | const pincodeUrl = `/pincode/${currMerchId}/${pincode}`; 62 | const pincodeRes = await makeGETRequest(pincodeUrl); 63 | if (pincodeRes["serviced"]) { 64 | return currMerchId; 65 | } else { 66 | return null; 67 | } 68 | } catch (err) { 69 | console.log(`Error checking pincode for merchantId ${currMerchId}: `, err); 70 | return null; 71 | } 72 | }); 73 | 74 | 75 | // Awaiting for real-time parallel O(1) queries, 76 | // for each merchant of this product against the Redis pincode set. 77 | const serviceableMerchants = (await Promise.all(serviceableMerchantsPromises)).filter(Boolean); 78 | 79 | 80 | console.log("Merchants that deliver to pincode: ", serviceableMerchants); 81 | // Displaying status for delivery premises (YES or NO). 82 | if (serviceableMerchants.length === 0) { 83 | setShowUnderNonDeliverable(true); 84 | setShowUnderDeliverable(false); 85 | setBlurButtons(true); 86 | return; 87 | } else { 88 | setShowUnderDeliverable(true); 89 | setShowUnderNonDeliverable(false); 90 | } 91 | 92 | 93 | // Pulling merchant details asynchronously in parallel. 94 | const fetchedOkMerchantsPromises = serviceableMerchants.map( async (currServiceableMerchId) => { 95 | try { 96 | // GET request for Product API. 97 | const merchantUrl = `/merchant/merchants/${currServiceableMerchId}`; 98 | const merchantResponse = await makeGETRequest(merchantUrl); 99 | // GET request for Quote API. 100 | const quoteUrl = `/quote/getQuote?p=${productId}&m=${currServiceableMerchId}`; 101 | const quoteResponse = await makeGETRequest(quoteUrl); 102 | // Creating Merchant Details to show. 103 | let merchantDetails = { 104 | merchantId: merchantResponse["_id"], 105 | merchantName: merchantResponse["merchantName"], 106 | merchantDescription: merchantResponse["merchantDescription"], 107 | sellingPrice: quoteResponse["sellingPrice"] 108 | } 109 | return merchantDetails; 110 | } catch (err) { 111 | console.log(`Error in fetching merchant and quote details for the merchant ID: ${currServiceableMerchId}: ${err}`); 112 | return null; 113 | } 114 | }); 115 | 116 | 117 | // Awaiting for all the merchant and price details together. 118 | const fetchedOkMerchants = (await Promise.all(fetchedOkMerchantsPromises)).filter(Boolean); 119 | 120 | // For testing. 121 | console.log("OK Merchants are: ", fetchedOkMerchants); 122 | 123 | 124 | setOkMerchants(fetchedOkMerchants); 125 | 126 | 127 | }; 128 | 129 | 130 | const returnMerchantIds = async (prodId) => { 131 | try { 132 | const url = "/product/items/" + prodId; 133 | const response = await makeGETRequest(url); 134 | console.log(response); 135 | const fetchResponse = response["merchantIds"]; 136 | return fetchResponse; 137 | } catch (error) { 138 | console.error('Error fetching product:', error); 139 | } 140 | } 141 | 142 | 143 | const handlePincodeChange = (e) => { 144 | setPincode(e.target.value); 145 | setChecked(false); // Reset the check state when pincode changes 146 | setShowUnderDeliverable(false); 147 | setShowUnderNonDeliverable(false); 148 | }; 149 | 150 | 151 | return ( 152 |
153 |
154 |
155 | pinpoint 156 |
157 |
158 | product 159 |
160 |
161 |
162 |
163 |
164 |
165 |

Products

166 |

Shops

167 |

Details

168 | 169 |
170 |
171 | 172 |
173 |
174 |

175 |
176 | 177 |

catalog

178 |
179 |
180 |

Sternglas zirkel

181 |
182 |
183 |
184 | 185 |
186 |
187 | 188 |
189 |
190 | 191 |
192 |
193 |

size:

194 |

40

195 | 196 |
197 |
198 |
199 |

200 |

49990

201 |
202 |
203 |

Check delivery availability here:

204 |
205 |
206 |
207 | 208 | 214 |
215 |
216 |

check

217 |
218 | {showUnderNonDeliverable && checked && ( 219 |
220 |

Outside deliverable premises

221 | 222 |
223 | )} 224 | {showUnderDeliverable && checked && ( 225 |
226 |

Under deliverable premises

227 | 228 |
229 | )} 230 |
231 | {showUnderNonDeliverable && checked && ( 232 |
233 |

No seller found

234 |
235 | )} 236 | {showUnderDeliverable && checked && ( 237 |
238 |

Here's the list of merchants selling Sternglas zirkel

239 |
240 | )} 241 |
242 | { 243 | okMerchants.map((okMerchant) => { 244 | return () 245 | }) 246 | } 247 |
248 |
249 |
250 |

Buy Now

251 | 252 |
253 |
254 | 255 |

Add to cart

256 |
257 |
258 |
259 |

260 | The minimalist among minimalists in a brushed case with a 40 mm 261 | diameter. The reduction to the essential forms the character of the 262 | ZIRKEL. Besides, the new 2019 version with the Miyota 9015 263 | movement is 17% thinner than its predecessor. A round affair. 264 |

265 |
266 |

267 |
268 |
269 |
270 | ); 271 | }; 272 | 273 | 274 | export default Ecommerce; -------------------------------------------------------------------------------- /server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | APIs 8 | 92 | 93 | 94 | 95 |
96 |

Welcome to Pinpoint Server Side APIs!

97 | 98 | 99 |
100 |

Pincode API

101 | 102 | 103 |
104 |
105 |
106 | 107 | GET /api/pincode/:merchantId/:pincode 108 | 109 |
110 |

Check if a pincode is serviced by a merchant.

111 |

Example Response (Pincode is serviced):

112 |

113 | {
114 |   "serviced": true
115 | }
116 | 
117 |

Example Response (Pincode is not serviced):

118 |

119 | {
120 |   "serviced": false
121 | }
122 | 
123 |
124 |
125 |
126 | 127 | GET /api/pincode/:merchantId 128 | 129 |
130 |

Returns a list of pincodes serviced by the merchant in a sorted manner.

131 |

Example Response:

132 |

133 | {
134 |     "pincodesList": [
135 |         "110001",
136 |         "110064"
137 |     ]
138 | }
139 | 
140 |
141 |
142 |
143 | 144 | POST /api/pincode/:merchantId/:pincode 145 | 146 |
147 |

Inserts a single pincode to the Redis set against the key merchant:${merchantId}

148 |

Example Response:

149 |

150 |   'Pincode ${pincode} added to set for merchant ${merchantId}'
151 |   
152 |
153 |
154 |
155 | 156 | POST /api/pincode/:merchantId 157 | 158 |
159 |

Upload a CSV file containing the pincodes for a merchant under the column named pincode 160 | and set pincodes for a merchant with the provided ID. The pincodes are 161 | added as values in a Redis set against the key merchant:${merchantId}

162 |

Example Request:

163 |

164 | FormData:
165 |   - csvFile: [CSV File]
166 | 
167 |

Example Response:

168 |

169 | 'CSV processing complete, added the pincodes successfully'
170 | 
171 |

Example of csvFile in request:

172 | Example Image 173 |
174 |
175 | 176 |
177 | 178 | 179 |
180 |

Product API

181 | 182 |
183 |
184 |
185 | 186 | GET /api/product/items 187 | 188 |
189 |

Return all the products. Example:

190 |

191 | [
192 |   {
193 |     "_id": "abcd1",
194 |     "merchantIds": [],
195 |     "productName": "Product 1",
196 |     "imageLink": "Link",
197 |     "productDescription": "Product 1 description",
198 |     "createdAt": "YYY-MM-DDTHH:MM:SS.285Z",
199 |     "updatedAt": "YYY-MM-DDTHH:MM:SS.285Z",
200 |     "__v": 0
201 |   },
202 |   {
203 |     "_id": "abcd2",
204 |     "merchantIds": [],
205 |     "productName": "Product 2",
206 |     "imageLink": "Link",
207 |     "productDescription": "Product 2 description",
208 |     "createdAt": "YYY-MM-DDTHH:MM:SS.285Z",
209 |     "updatedAt": "YYY-MM-DDTHH:MM:SS.285Z",
210 |     "__v": 0
211 |   }
212 | ]
213 |           
214 |
215 |
216 |
217 | 218 | GET /api/product/items/:id 219 | 220 |
221 |

Retrieve detailed information about a specific product based on its unique identifier. 222 | Example:

223 |

224 | {
225 |   "_id": "abcd1",
226 |   "merchantIds": [],
227 |   "productName": "Product 1",
228 |   "imageLink": "Link",
229 |   "productDescription": "Product 1 description",
230 |   "createdAt": "YYY-MM-DDTHH:MM:SS.285Z",
231 |   "updatedAt": "YYY-MM-DDTHH:MM:SS.285Z",
232 |   "__v": 0
233 | }
234 |           
235 |
236 |
237 |
238 | 239 | POST /api/product/create 240 | 241 |
242 |

Create a new product with the provided details. Request body:

243 |

244 | {
245 |   "productName": "New Product",
246 |   "imageLink": "Image Link",
247 |   "productDescription": "Product Description"
248 | }
249 |           
250 |
251 |
252 |
253 | 254 | POST /api/product/insert/:id 255 | 256 |
257 |

Append a list of merchant IDs to an existing product. Request body:

258 |

259 | {
260 |   "merchantsArr": ["merchantId1", "merchantId2", "merchantId3"]
261 | }
262 |           
263 |
264 |
265 |
266 | 267 | 268 |
269 |

Merchant API

270 | 271 |
272 |
273 |
274 | 275 | GET /api/merchant/merchants 276 | 277 |
278 |

Get a list of all merchants.

279 |

Example Response:

280 |

281 |             [
282 |               {
283 |                 "_id": "merchantId1",
284 |                 "merchantName": "Merchant 1",
285 |                 "merchantAddress": "City, Country",
286 |                 "merchantDescription": "Description for Merchant 1",
287 |                 "createdAt": "YYYY-MM-DDT12:34:56.789Z",
288 |                 "updatedAt": "YYYY-MM-DDT13:45:12.345Z",
289 |                 "__v": 0
290 |               },
291 |               {
292 |                 "_id": "merchantId2",
293 |                 "merchantName": "Merchant 2",
294 |                 "merchantAddress": "City, Country",
295 |                 "merchantDescription": "Description for Merchant 2",
296 |                 "createdAt": "YYYY-MM-DDT14:56:23.456Z",
297 |                 "updatedAt": "YYYY-MM-DDT16:12:34.567Z",
298 |                 "__v": 0
299 |               }
300 |             ]
301 |           
302 |
303 |
304 |
305 | 306 | GET /api/merchant/merchants/:id 307 | 308 |
309 |

Get specific details of a merchant by its ID.

310 |

Example Response:

311 |

312 |             {
313 |               "_id": "merchantId1",
314 |               "merchantName": "Merchant 1",
315 |               "merchantAddress": "City, Country",
316 |               "merchantDescription": "Description for Merchant 1",
317 |               "createdAt": "YYYY-MM-DDT12:34:56.789Z",
318 |               "updatedAt": "YYYY-MM-DDT13:45:12.345Z",
319 |               "__v": 0
320 |             }
321 |           
322 |
323 |
324 |
325 | 326 | POST /api/merchant/create 327 | 328 |
329 |

Create a new merchant with the provided details.

330 |

Example Request:

331 |

332 |             {
333 |               "merchantName": "New Merchant",
334 |               "merchantAddress": "City, Country",
335 |               "merchantDescription": "Description for New Merchant"
336 |             }
337 |           
338 |

Example Response:

339 |

340 |             {
341 |               "_id": "newMerchantId",
342 |               "merchantName": "New Merchant",
343 |               "merchantAddress": "City, Country",
344 |               "merchantDescription": "Description for New Merchant",
345 |               "createdAt": "YYYY-MM-DDT10:12:34.567Z",
346 |               "updatedAt": "YYYY-MM-DDT11:23:45.678Z",
347 |               "__v": 0
348 |             }
349 |           
350 |
351 |
352 |
353 | 354 |
355 |

Quote API

356 | 357 |
358 |
359 |
360 | 361 | GET /api/quote/getQuote?p=productId&m=merchantId 362 | 363 |
364 |

Get the price quote for a merchant and product delivered by them.

365 |

Query Parameters:

366 |

367 |             {
368 |               "p": "productId1",
369 |               "m": "merchantId1"
370 |             }
371 |           
372 |

Example Response:

373 |

374 |             {
375 |               "_id": "quoteId1",
376 |               "productId": "productId1",
377 |               "merchantId": "merchantId1",
378 |               "sellingPrice": 49.99,
379 |               "createdAt": "YYYY-MM-DDT08:45:12.345Z",
380 |               "updatedAt": "YYYY-MM-DDT09:56:23.456Z",
381 |               "__v": 0
382 |             }
383 |           
384 |
385 |
386 |
387 | 388 | POST /api/quote/create/:merchantId 389 | 390 |
391 |

Create a new quote for a merchant with ID ${merchantId}.

392 |

Example Request body:

393 |

394 |             {
395 |               "productId": "productId1",
396 |               "sellingPrice": 70000
397 |             }
398 |           
399 |

Example Response:

400 |

401 |             {
402 |               "_id": "newQuoteId",
403 |               "productId": "productId1",
404 |               "merchantId": "merchantId1",
405 |               "sellingPrice": 70000,
406 |               "createdAt": "YYYY-MM-DDT10:12:34.567Z",
407 |               "updatedAt": "YYYY-MM-DDT11:23:45.678Z",
408 |               "__v": 0
409 |             }
410 |           
411 |
412 |
413 |
414 | 415 |
416 | 417 | 433 | 434 | 435 | -------------------------------------------------------------------------------- /client/src/assets/images/ondc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------