Compare commits
9 Commits
38453e7f6d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fa3a57e84 | ||
|
|
f898adff22 | ||
|
|
2bf455f53f | ||
|
|
f876fa9239 | ||
|
|
d07ae358eb | ||
|
|
c1b6d7a7cb | ||
|
|
6750c5456f | ||
|
|
1a000a3be2 | ||
|
|
a330844138 |
2
.gitignore
vendored
@@ -3,5 +3,3 @@
|
||||
backend/razorpay.env
|
||||
backend/src/main/resources/application-local.properties
|
||||
counter-frontend/.env
|
||||
ordering-site/*
|
||||
ordering_site/
|
||||
|
||||
@@ -4,7 +4,7 @@ server.port=8080
|
||||
|
||||
# ============================================================
|
||||
# DATABASE — REQUIRED environment variables
|
||||
# Set these in your environment or a .env file
|
||||
# Set these in your environment or a .env file.
|
||||
# ============================================================
|
||||
spring.datasource.url=jdbc:postgresql://localhost:5432/rit_cms
|
||||
spring.datasource.username=postgres
|
||||
@@ -20,7 +20,7 @@ spring.jpa.properties.hibernate.jdbc.time_zone=Asia/Kolkata
|
||||
# JWT — REQUIRED environment variables
|
||||
# MUST provide a secure random key (min 256-bit)
|
||||
# ============================================================
|
||||
app.jwt.secret=${JWT_SECRET:default_jwt_secret_key_which_is_at_least_32_bytes_long}
|
||||
app.jwt.secret=${JWT_SECRET:f4d9b0d81b6d6ef8c8d6f35f4fb2bb2b9e2fd6f7d20a0c4d0b6d97c93e4f6f7a9d2b8f4a7e1c5d9f3b6a2c8e4f7d1b9c0a6e3d8f5b2c7a1e9d4f6c8b0a3e7f1}
|
||||
app.jwt.expiration-ms=86400000
|
||||
|
||||
# ============================================================
|
||||
|
||||
9
counter-frontend/dist/assets/AnimatePresence-CcsC7tO8.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import{a as e,i as t,n,o as r,r as i}from"./proxy-cLldRXEc.js";import{g as a,h as o,m as s,p as c,u as l}from"./index-B-Yfb7Zz.js";var u=s(),d=a(o(),1);function f(e,t){if(typeof e==`function`)return e(t);e!=null&&(e.current=t)}function p(...e){return t=>{let n=!1,r=e.map(e=>{let r=f(e,t);return!n&&typeof r==`function`&&(n=!0),r});if(n)return()=>{for(let t=0;t<r.length;t++){let n=r[t];typeof n==`function`?n():f(e[t],null)}}}}function m(...e){return d.useCallback(p(...e),e)}var h=class extends d.Component{getSnapshotBeforeUpdate(e){let t=this.props.childRef.current;if(i(t)&&e.isPresent&&!this.props.isPresent&&this.props.pop!==!1){let e=t.offsetParent,n=i(e)&&e.offsetWidth||0,r=i(e)&&e.offsetHeight||0,a=getComputedStyle(t),o=this.props.sizeRef.current;o.height=parseFloat(a.height),o.width=parseFloat(a.width),o.top=t.offsetTop,o.left=t.offsetLeft,o.right=n-o.width-o.left,o.bottom=r-o.height-o.top}return null}componentDidUpdate(){}render(){return this.props.children}};function g({children:e,isPresent:t,anchorX:n,anchorY:r,root:i,pop:a}){let o=(0,d.useId)(),s=(0,d.useRef)(null),c=(0,d.useRef)({width:0,height:0,top:0,left:0,right:0,bottom:0}),{nonce:f}=(0,d.useContext)(l),p=m(s,e.props?.ref??e?.ref);return(0,d.useInsertionEffect)(()=>{let{width:e,height:l,top:u,left:d,right:p,bottom:m}=c.current;if(t||a===!1||!s.current||!e||!l)return;let h=n===`left`?`left: ${d}`:`right: ${p}`,g=r===`bottom`?`bottom: ${m}`:`top: ${u}`;s.current.dataset.motionPopId=o;let _=document.createElement(`style`);f&&(_.nonce=f);let v=i??document.head;return v.appendChild(_),_.sheet&&_.sheet.insertRule(`
|
||||
[data-motion-pop-id="${o}"] {
|
||||
position: absolute !important;
|
||||
width: ${e}px !important;
|
||||
height: ${l}px !important;
|
||||
${h}px !important;
|
||||
${g}px !important;
|
||||
}
|
||||
`),()=>{s.current?.removeAttribute(`data-motion-pop-id`),v.contains(_)&&v.removeChild(_)}},[t]),(0,u.jsx)(h,{isPresent:t,childRef:s,sizeRef:c,pop:a,children:a===!1?e:d.cloneElement(e,{ref:p})})}var _=({children:e,initial:n,isPresent:r,onExitComplete:i,custom:a,presenceAffectsLayout:o,mode:s,anchorX:l,anchorY:f,root:p})=>{let m=c(v),h=(0,d.useId)(),_=!0,y=(0,d.useMemo)(()=>(_=!1,{id:h,initial:n,isPresent:r,custom:a,onExitComplete:e=>{m.set(e,!0);for(let e of m.values())if(!e)return;i&&i()},register:e=>(m.set(e,!1),()=>m.delete(e))}),[r,m,i]);return o&&_&&(y={...y}),(0,d.useMemo)(()=>{m.forEach((e,t)=>m.set(t,!1))},[r]),d.useEffect(()=>{!r&&!m.size&&i&&i()},[r]),e=(0,u.jsx)(g,{pop:s===`popLayout`,isPresent:r,anchorX:l,anchorY:f,root:p,children:e}),(0,u.jsx)(t.Provider,{value:y,children:e})};function v(){return new Map}var y=e=>e.key||``;function b(e){let t=[];return d.Children.forEach(e,e=>{(0,d.isValidElement)(e)&&t.push(e)}),t}var x=({children:t,custom:i,initial:a=!0,onExitComplete:o,presenceAffectsLayout:s=!0,mode:l=`sync`,propagate:f=!1,anchorX:p=`left`,anchorY:m=`top`,root:h})=>{let[g,v]=n(f),x=(0,d.useMemo)(()=>b(t),[t]),S=f&&!g?[]:x.map(y),C=(0,d.useRef)(!0),w=(0,d.useRef)(x),T=c(()=>new Map),E=(0,d.useRef)(new Set),[D,O]=(0,d.useState)(x),[k,A]=(0,d.useState)(x);e(()=>{C.current=!1,w.current=x;for(let e=0;e<k.length;e++){let t=y(k[e]);S.includes(t)?(T.delete(t),E.current.delete(t)):T.get(t)!==!0&&T.set(t,!1)}},[k,S.length,S.join(`-`)]);let j=[];if(x!==D){let e=[...x];for(let t=0;t<k.length;t++){let n=k[t],r=y(n);S.includes(r)||(e.splice(t,0,n),j.push(n))}return l===`wait`&&j.length&&(e=j),A(b(e)),O(x),null}let{forceRender:M}=(0,d.useContext)(r);return(0,u.jsx)(u.Fragment,{children:k.map(e=>{let t=y(e),n=f&&!g?!1:x===k||S.includes(t);return(0,u.jsx)(_,{isPresent:n,initial:!C.current||a?void 0:!1,custom:i,presenceAffectsLayout:s,mode:l,root:h,onExitComplete:n?void 0:()=>{if(E.current.has(t))return;if(T.has(t))E.current.add(t),T.set(t,!0);else return;let e=!0;T.forEach(t=>{t||(e=!1)}),e&&(M?.(),A(w.current),f&&v?.(),o&&o())},anchorX:p,anchorY:m,children:e},t)})})};export{x as t};
|
||||
1
counter-frontend/dist/assets/Categories-DzWzJLW4.js
vendored
Normal file
1
counter-frontend/dist/assets/Login-D9pMAuPm.js
vendored
Normal file
71
counter-frontend/dist/assets/Orders-BqkxCVX3.js
vendored
Normal file
7
counter-frontend/dist/assets/POS-Bp9lDbPa.js
vendored
Normal file
1
counter-frontend/dist/assets/Pagination-BYvNtzJC.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{h as e,m as t,s as n}from"./index-B-Yfb7Zz.js";var r=n(`chevron-left`,[[`path`,{d:`m15 18-6-6 6-6`,key:`1wnfg3`}]]),i=n(`chevron-right`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]);e();var a=t(),o=({currentPage:e,pageSize:t,totalElements:n,onPageChange:o,onPageSizeChange:s})=>{let c=Math.ceil(n/t),l=e*t+1,u=Math.min((e+1)*t,n);return n===0?null:(0,a.jsxs)(`div`,{className:`px-8 py-6 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-6 bg-white shrink-0`,children:[(0,a.jsxs)(`div`,{className:`flex items-center gap-4`,children:[(0,a.jsx)(`span`,{className:`text-xs font-black text-slate-400 uppercase tracking-widest leading-none`,children:`Show`}),(0,a.jsx)(`select`,{value:t,onChange:e=>s(Number(e.target.value)),className:`bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-xs font-black text-slate-900 outline-none focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all appearance-none pr-8 relative`,style:{backgroundImage:`url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,backgroundRepeat:`no-repeat`,backgroundPosition:`right 0.5rem center`,backgroundSize:`1rem`},children:[5,10,25,50].map(e=>(0,a.jsxs)(`option`,{value:e,children:[e,` items`]},e))}),(0,a.jsxs)(`span`,{className:`text-[10px] font-bold text-slate-300 uppercase tracking-widest whitespace-nowrap`,children:[l,`-`,u,` of `,n,` units`]})]}),(0,a.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,a.jsx)(`button`,{disabled:e===0,onClick:()=>o(e-1),className:`w-10 h-10 flex items-center justify-center rounded-xl border border-slate-100 text-slate-400 hover:text-primary hover:bg-primary/5 disabled:opacity-30 disabled:hover:bg-transparent transition-all`,children:(0,a.jsx)(r,{size:18})}),(0,a.jsx)(`div`,{className:`flex items-center gap-1`,children:Array.from({length:c},(t,n)=>(0,a.jsx)(`button`,{onClick:()=>o(n),className:`w-10 h-10 font-bold text-xs rounded-xl transition-all ${e===n?`bg-primary text-white shadow-lg shadow-primary/20`:`text-slate-400 hover:bg-slate-50`}`,children:n+1},n)).slice(Math.max(0,e-1),Math.min(c,e+2))}),(0,a.jsx)(`button`,{disabled:e>=c-1,onClick:()=>o(e+1),className:`w-10 h-10 flex items-center justify-center rounded-xl border border-slate-100 text-slate-400 hover:text-primary hover:bg-primary/5 disabled:opacity-30 disabled:hover:bg-transparent transition-all`,children:(0,a.jsx)(i,{size:18})})]})]})};export{o as t};
|
||||
5
counter-frontend/dist/assets/Products-Cs9hGefr.js
vendored
Normal file
12
counter-frontend/dist/assets/index-B-Yfb7Zz.js
vendored
Normal file
99
counter-frontend/dist/assets/index-C3UP9F-T.js
vendored
2
counter-frontend/dist/assets/index-HWj_Y8q4.css
vendored
Normal file
1
counter-frontend/dist/assets/minus-wD8f4ZO0.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{s as e}from"./index-B-Yfb7Zz.js";var t=e(`minus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}]]);export{t};
|
||||
1
counter-frontend/dist/assets/plus-CtPxjhzP.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{s as e}from"./index-B-Yfb7Zz.js";var t=e(`plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]);export{t};
|
||||
1
counter-frontend/dist/assets/proxy-cLldRXEc.js
vendored
Normal file
BIN
counter-frontend/dist/assets/ritchennai-Cj5SDFG-.webp
vendored
Normal file
|
After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
BIN
counter-frontend/dist/assets/tillo_spinner-BTedCmel.gif
vendored
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 4.8 MiB |
1
counter-frontend/dist/assets/trash-2-B3saCmcm.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{s as e}from"./index-B-Yfb7Zz.js";var t=e(`trash-2`,[[`path`,{d:`M10 11v6`,key:`nco0om`}],[`path`,{d:`M14 11v6`,key:`outv1u`}],[`path`,{d:`M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6`,key:`miytrc`}],[`path`,{d:`M3 6h18`,key:`d0wm0j`}],[`path`,{d:`M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2`,key:`e791ji`}]]);export{t};
|
||||
1
counter-frontend/dist/assets/x-CnVgnJPD.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{s as e}from"./index-B-Yfb7Zz.js";var t=e(`check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),n=e(`pen`,[[`path`,{d:`M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z`,key:`1a8usu`}]]),r=e(`x`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]);export{n,t as r,r as t};
|
||||
4
counter-frontend/dist/index.html
vendored
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Counter POS</title>
|
||||
<script type="module" crossorigin src="/assets/index-C3UP9F-T.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-8tt0cn-G.css">
|
||||
<script type="module" crossorigin src="/assets/index-B-Yfb7Zz.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-HWj_Y8q4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,35 +1,50 @@
|
||||
import React from 'react';
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { MotionConfig } from 'framer-motion';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import Login from './pages/Login';
|
||||
import POS from './pages/POS';
|
||||
import Orders from './pages/Orders';
|
||||
import Categories from './pages/Categories';
|
||||
import Products from './pages/Products';
|
||||
import Layout from './components/Layout';
|
||||
|
||||
// Lazy load pages to decrease initial bundle size
|
||||
const Login = lazy(() => import('./pages/Login'));
|
||||
const POS = lazy(() => import('./pages/POS'));
|
||||
const Orders = lazy(() => import('./pages/Orders'));
|
||||
const Categories = lazy(() => import('./pages/Categories'));
|
||||
const Products = lazy(() => import('./pages/Products'));
|
||||
|
||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
const isLoggedIn = sessionStorage.getItem('isCounterLoggedIn') === 'true';
|
||||
return isLoggedIn ? <>{children}</> : <Navigate to="/login" replace />;
|
||||
};
|
||||
|
||||
const LoadingFallback = () => (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#f4fbf7]">
|
||||
<div className="text-primary font-black uppercase tracking-widest text-sm animate-pulse">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<Layout />
|
||||
</ProtectedRoute>
|
||||
}>
|
||||
<Route index element={<Navigate to="/pos" replace />} />
|
||||
<Route path="pos" element={<POS />} />
|
||||
<Route path="orders" element={<Orders />} />
|
||||
<Route path="categories" element={<Categories />} />
|
||||
<Route path="products" element={<Products />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
<MotionConfig skipAnimations={true}>
|
||||
<Router>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<Layout />
|
||||
</ProtectedRoute>
|
||||
}>
|
||||
<Route index element={<Navigate to="/pos" replace />} />
|
||||
<Route path="pos" element={<POS />} />
|
||||
<Route path="orders" element={<Orders />} />
|
||||
<Route path="categories" element={<Categories />} />
|
||||
<Route path="products" element={<Products />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Router>
|
||||
</MotionConfig>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 4.8 MiB After Width: | Height: | Size: 41 KiB |
@@ -26,7 +26,7 @@ const Login = () => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const host = window.location.hostname;
|
||||
|
||||
const response = await fetch(`https://cmsapi.rit-services.in/api/system/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -1,102 +1,115 @@
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { MotionConfig } from 'framer-motion';
|
||||
import Layout from './components/Layout.tsx';
|
||||
import Dashboard from './pages/Dashboard.tsx';
|
||||
import BaseMenu from './pages/BaseMenu.tsx';
|
||||
import Products from './pages/Products.tsx';
|
||||
import Orders from './pages/Orders.tsx';
|
||||
import ArchivedOrders from './pages/ArchivedOrders.tsx';
|
||||
import Customers from './pages/Customers.tsx';
|
||||
import Login from './pages/Login.tsx';
|
||||
import Managers from './pages/Managers.tsx';
|
||||
import Stalls from './pages/Stalls.tsx';
|
||||
import Staff from './pages/Staff.tsx';
|
||||
import Terminals from './pages/Terminals.tsx';
|
||||
import Vendors from './pages/Vendors.tsx';
|
||||
import Purchases from './pages/Purchases.tsx';
|
||||
import VendorDashboard from './pages/VendorDashboard.tsx';
|
||||
import PurchaseAnalytics from './pages/PurchaseAnalytics.tsx';
|
||||
import Bills from './pages/Bills.tsx';
|
||||
import PurchaseSummary from './pages/PurchaseSummary.tsx';
|
||||
import IntentDashboard from './pages/IntentDashboard.tsx';
|
||||
import IntentList from './pages/IntentList.tsx';
|
||||
import NewArrivals from './pages/NewArrivals.tsx';
|
||||
import Reports from './pages/Reports.tsx';
|
||||
import Feedback from './pages/Feedback.tsx';
|
||||
import Ritz from './pages/Ritz.tsx';
|
||||
import RitzCirculation from './pages/RitzCirculation.tsx';
|
||||
import ManageWallets from './pages/ManageWallets.tsx';
|
||||
import ManageCoupons from './pages/ManageCoupons.tsx';
|
||||
import Settings from './pages/Settings.tsx';
|
||||
import NotFound from './pages/NotFound.tsx';
|
||||
|
||||
// Lazy load pages to decrease initial bundle size
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard.tsx'));
|
||||
const BaseMenu = lazy(() => import('./pages/BaseMenu.tsx'));
|
||||
const Products = lazy(() => import('./pages/Products.tsx'));
|
||||
const Orders = lazy(() => import('./pages/Orders.tsx'));
|
||||
const ArchivedOrders = lazy(() => import('./pages/ArchivedOrders.tsx'));
|
||||
const Customers = lazy(() => import('./pages/Customers.tsx'));
|
||||
const Login = lazy(() => import('./pages/Login.tsx'));
|
||||
const Managers = lazy(() => import('./pages/Managers.tsx'));
|
||||
const Stalls = lazy(() => import('./pages/Stalls.tsx'));
|
||||
const Staff = lazy(() => import('./pages/Staff.tsx'));
|
||||
const Terminals = lazy(() => import('./pages/Terminals.tsx'));
|
||||
const Vendors = lazy(() => import('./pages/Vendors.tsx'));
|
||||
const Purchases = lazy(() => import('./pages/Purchases.tsx'));
|
||||
const VendorDashboard = lazy(() => import('./pages/VendorDashboard.tsx'));
|
||||
const PurchaseAnalytics = lazy(() => import('./pages/PurchaseAnalytics.tsx'));
|
||||
const Bills = lazy(() => import('./pages/Bills.tsx'));
|
||||
const PurchaseSummary = lazy(() => import('./pages/PurchaseSummary.tsx'));
|
||||
const IntentDashboard = lazy(() => import('./pages/IntentDashboard.tsx'));
|
||||
const IntentList = lazy(() => import('./pages/IntentList.tsx'));
|
||||
const NewArrivals = lazy(() => import('./pages/NewArrivals.tsx'));
|
||||
const Reports = lazy(() => import('./pages/Reports.tsx'));
|
||||
const Feedback = lazy(() => import('./pages/Feedback.tsx'));
|
||||
const Ritz = lazy(() => import('./pages/Ritz.tsx'));
|
||||
const RitzCirculation = lazy(() => import('./pages/RitzCirculation.tsx'));
|
||||
const ManageWallets = lazy(() => import('./pages/ManageWallets.tsx'));
|
||||
const ManageCoupons = lazy(() => import('./pages/ManageCoupons.tsx'));
|
||||
const Settings = lazy(() => import('./pages/Settings.tsx'));
|
||||
const NotFound = lazy(() => import('./pages/NotFound.tsx'));
|
||||
|
||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true';
|
||||
return isLoggedIn ? <>{children}</> : <Navigate to="/login" replace />;
|
||||
};
|
||||
|
||||
const LoadingFallback = () => (
|
||||
<div className="min-h-screen flex items-center justify-center bg-slate-50/50">
|
||||
<div className="text-primary font-black uppercase tracking-widest text-sm animate-pulse">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<Layout />
|
||||
</ProtectedRoute>
|
||||
}>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<MotionConfig skipAnimations={true}>
|
||||
<Router>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<Layout />
|
||||
</ProtectedRoute>
|
||||
}>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
|
||||
{/* Sale */}
|
||||
<Route path="sale/orders" element={<Orders />} />
|
||||
<Route path="sale/archived-bills" element={<ArchivedOrders />} />
|
||||
{/* Sale */}
|
||||
<Route path="sale/orders" element={<Orders />} />
|
||||
<Route path="sale/archived-bills" element={<ArchivedOrders />} />
|
||||
|
||||
{/* Customers */}
|
||||
<Route path="customers/orders" element={<Customers />} />
|
||||
{/* Customers */}
|
||||
<Route path="customers/orders" element={<Customers />} />
|
||||
|
||||
{/* Purchases */}
|
||||
<Route path="purchases/dashboard" element={<VendorDashboard />} />
|
||||
<Route path="purchases/orders" element={<Purchases />} />
|
||||
<Route path="purchases/summary" element={<PurchaseSummary />} />
|
||||
<Route path="purchases/bills" element={<Bills />} />
|
||||
<Route path="purchases/vendor" element={<Vendors />} />
|
||||
<Route path="purchases/analytics" element={<PurchaseAnalytics />} />
|
||||
{/* Purchases */}
|
||||
<Route path="purchases/dashboard" element={<VendorDashboard />} />
|
||||
<Route path="purchases/orders" element={<Purchases />} />
|
||||
<Route path="purchases/summary" element={<PurchaseSummary />} />
|
||||
<Route path="purchases/bills" element={<Bills />} />
|
||||
<Route path="purchases/vendor" element={<Vendors />} />
|
||||
<Route path="purchases/analytics" element={<PurchaseAnalytics />} />
|
||||
|
||||
{/* Intent */}
|
||||
<Route path="purchases/intent/orders-dashboard" element={<IntentDashboard title="ORDER DASHBOARD" />} />
|
||||
<Route path="purchases/intent/receives-dashboard" element={<IntentDashboard title="RECEIVABLE DASHBOARD" />} />
|
||||
<Route path="purchases/intent/orders" element={<IntentList title="ORDERS" />} />
|
||||
<Route path="purchases/intent/receives" element={<IntentList title="RECEIVES" />} />
|
||||
{/* Intent */}
|
||||
<Route path="purchases/intent/orders-dashboard" element={<IntentDashboard title="ORDER DASHBOARD" />} />
|
||||
<Route path="purchases/intent/receives-dashboard" element={<IntentDashboard title="RECEIVABLE DASHBOARD" />} />
|
||||
<Route path="purchases/intent/orders" element={<IntentList title="ORDERS" />} />
|
||||
<Route path="purchases/intent/receives" element={<IntentList title="RECEIVES" />} />
|
||||
|
||||
{/* Inventory */}
|
||||
<Route path="inventory/new-arrivals" element={<NewArrivals />} />
|
||||
<Route path="inventory/base" element={<BaseMenu />} />
|
||||
<Route path="inventory/products" element={<Products />} />
|
||||
<Route path="inventory/online" element={<Navigate to="/inventory/products" replace />} />
|
||||
|
||||
{/* Inventory */}
|
||||
<Route path="inventory/new-arrivals" element={<NewArrivals />} />
|
||||
<Route path="inventory/base" element={<BaseMenu />} />
|
||||
<Route path="inventory/products" element={<Products />} />
|
||||
<Route path="inventory/online" element={<Navigate to="/inventory/products" replace />} />
|
||||
{/* Others */}
|
||||
<Route path="reports" element={<Reports />} />
|
||||
<Route path="feedback" element={<Feedback />} />
|
||||
<Route path="ritz/overview" element={<Ritz />} />
|
||||
<Route path="ritz/circulation" element={<RitzCirculation />} />
|
||||
<Route path="ritz/wallets" element={<ManageWallets />} />
|
||||
<Route path="ritz/coupons" element={<ManageCoupons />} />
|
||||
|
||||
{/* Others */}
|
||||
<Route path="reports" element={<Reports />} />
|
||||
<Route path="feedback" element={<Feedback />} />
|
||||
<Route path="ritz/overview" element={<Ritz />} />
|
||||
<Route path="ritz/circulation" element={<RitzCirculation />} />
|
||||
<Route path="ritz/wallets" element={<ManageWallets />} />
|
||||
<Route path="ritz/coupons" element={<ManageCoupons />} />
|
||||
{/* Stores */}
|
||||
<Route path="stores/terminals" element={<Terminals />} />
|
||||
<Route path="stores/managers" element={<Managers />} />
|
||||
<Route path="stores/staffs" element={<Staff />} />
|
||||
<Route path="stores/stalls" element={<Stalls />} />
|
||||
|
||||
|
||||
{/* Stores */}
|
||||
<Route path="stores/terminals" element={<Terminals />} />
|
||||
<Route path="stores/managers" element={<Managers />} />
|
||||
<Route path="stores/staffs" element={<Staff />} />
|
||||
<Route path="stores/stalls" element={<Stalls />} />
|
||||
|
||||
{/* Settings */}
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
{/* Settings */}
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Router>
|
||||
</MotionConfig>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 371 KiB |
BIN
frontend/src/assets/RIT_Logo.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
BIN
frontend/src/assets/color-vector.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 375 KiB |
BIN
frontend/src/assets/food-court-pos.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 4.8 MiB After Width: | Height: | Size: 41 KiB |
@@ -19,7 +19,7 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import collegeLogo from '../assets/college-logo.png';
|
||||
import colorVector from '../assets/color-vector.png';
|
||||
import colorVector from '../assets/color-vector.webp';
|
||||
|
||||
function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
User,
|
||||
Lock,
|
||||
@@ -11,10 +10,10 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
// Import Assets
|
||||
// Import Assets (optimized WebP formats)
|
||||
import collegeLogo from '../assets/college-logo.png';
|
||||
import foodCourtPosBg from '../assets/food-court-pos.png';
|
||||
import ritLogo from '../assets/RIT_Logo.png';
|
||||
import foodCourtPosBg from '../assets/food-court-pos.webp';
|
||||
import ritLogo from '../assets/RIT_Logo.webp';
|
||||
|
||||
const Login = () => {
|
||||
const [role, setRole] = useState<'manager' | 'master'>('manager');
|
||||
@@ -76,25 +75,19 @@ const Login = () => {
|
||||
{/* Subtle light overlay to match the clean aesthetic and improve card contrast */}
|
||||
<div className="absolute inset-0 bg-white/20 backdrop-blur-[1px] z-0"></div>
|
||||
|
||||
{/* Login Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
{/* Login Card (Static layout, instant render) */}
|
||||
<div
|
||||
className="w-full max-w-md bg-white/90 backdrop-blur-md border border-white/20 rounded-3xl p-6 md:p-10 shadow-2xl flex flex-col relative z-10"
|
||||
>
|
||||
{/* Top Logo - Centered */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
<div
|
||||
className="flex flex-col items-center justify-center mb-8"
|
||||
>
|
||||
<img src={collegeLogo} alt="College Logo" className="h-16 md:h-20 object-contain filter drop-shadow-sm" />
|
||||
<span className="text-xs font-semibold text-[#001828]/60 mt-2.5 italic tracking-wide font-sans">
|
||||
Till yo tummy is full...
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Login Container */}
|
||||
<div className="w-full">
|
||||
@@ -107,14 +100,13 @@ const Login = () => {
|
||||
|
||||
{/* Role Toggle */}
|
||||
<div className="bg-slate-100/80 p-1 rounded-2xl flex gap-1 mb-6 relative">
|
||||
<motion.div
|
||||
className="absolute h-[calc(100%-8px)] top-1 bg-white rounded-xl shadow-sm z-0"
|
||||
initial={false}
|
||||
animate={{
|
||||
{/* Lightweight CSS transition for slider */}
|
||||
<div
|
||||
className="absolute h-[calc(100%-8px)] top-1 bg-white rounded-xl shadow-sm z-0 transition-all duration-200 ease-out"
|
||||
style={{
|
||||
left: role === 'manager' ? '4px' : '50%',
|
||||
width: 'calc(50% - 6px)'
|
||||
}}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setRole('manager')}
|
||||
@@ -194,11 +186,9 @@ const Login = () => {
|
||||
>
|
||||
<span className="relative z-10">CONNECT TO PORTAL</span>
|
||||
<ArrowRight size={18} className="relative z-10 group-hover:translate-x-1 transition-transform" />
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-white/10"
|
||||
initial={{ x: '-100%' }}
|
||||
whileHover={{ x: '100%' }}
|
||||
transition={{ duration: 0.6 }}
|
||||
{/* Lightweight CSS transition for button hover shine */}
|
||||
<div
|
||||
className="absolute inset-0 bg-white/10 -translate-x-full group-hover:translate-x-full transition-transform duration-500 ease-out z-0"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
@@ -210,20 +200,17 @@ const Login = () => {
|
||||
© 2026 TILLO • SYSTEM PORTAL
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Merchant Section Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
{/* Merchant Section Card (Static rendering) */}
|
||||
<div
|
||||
className="w-full max-w-md bg-white/80 backdrop-blur-md border border-white/20 rounded-2xl p-4 shadow-lg flex items-center justify-between gap-4 relative z-10"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Merchant Partner</span>
|
||||
</div>
|
||||
<img src={ritLogo} alt="RIT Logo" className="h-12 w-auto object-contain filter drop-shadow-sm" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Building2,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Fingerprint
|
||||
} from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import colorVector from '../assets/color-vector.png';
|
||||
import colorVector from '../assets/color-vector.webp';
|
||||
|
||||
interface TokenTransaction {
|
||||
id: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
CircleDollarSign,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
ChevronRight
|
||||
} from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import colorVector from '../assets/color-vector.png';
|
||||
import colorVector from '../assets/color-vector.webp';
|
||||
|
||||
interface TokenUnit {
|
||||
id: number;
|
||||
|
||||
@@ -1,110 +1,124 @@
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { MotionConfig } from 'framer-motion';
|
||||
import { CartProvider } from './contexts/CartContext';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { FoodProvider } from './contexts/FoodContext';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
import HomeScreen from './pages/HomeScreen';
|
||||
import CategoryScreen from './pages/CategoryScreen';
|
||||
import ItemDetailScreen from './pages/ItemDetailScreen';
|
||||
import CartScreen from './pages/CartScreen';
|
||||
import CheckoutScreen from './pages/CheckoutScreen';
|
||||
import SuccessScreen from './pages/SuccessScreen';
|
||||
import MyOrdersScreen from './pages/MyOrdersScreen';
|
||||
import LoginScreen from './pages/LoginScreen';
|
||||
import ProfileScreen from './pages/ProfileScreen';
|
||||
import ChangePinScreen from './pages/ChangePinScreen';
|
||||
import StallDetailScreen from './pages/StallDetailScreen';
|
||||
import WalletScreen from './pages/WalletScreen';
|
||||
import TopUpScreen from './pages/TopUpScreen';
|
||||
import PreferencesScreen from './pages/PreferencesScreen';
|
||||
import NotFoundScreen from './pages/NotFoundScreen';
|
||||
import StockAlert from './components/StockAlert';
|
||||
import './App.css';
|
||||
|
||||
// Lazy load screens
|
||||
const HomeScreen = lazy(() => import('./pages/HomeScreen'));
|
||||
const CategoryScreen = lazy(() => import('./pages/CategoryScreen'));
|
||||
const ItemDetailScreen = lazy(() => import('./pages/ItemDetailScreen'));
|
||||
const CartScreen = lazy(() => import('./pages/CartScreen'));
|
||||
const CheckoutScreen = lazy(() => import('./pages/CheckoutScreen'));
|
||||
const SuccessScreen = lazy(() => import('./pages/SuccessScreen'));
|
||||
const MyOrdersScreen = lazy(() => import('./pages/MyOrdersScreen'));
|
||||
const LoginScreen = lazy(() => import('./pages/LoginScreen'));
|
||||
const ProfileScreen = lazy(() => import('./pages/ProfileScreen'));
|
||||
const ChangePinScreen = lazy(() => import('./pages/ChangePinScreen'));
|
||||
const StallDetailScreen = lazy(() => import('./pages/StallDetailScreen'));
|
||||
const WalletScreen = lazy(() => import('./pages/WalletScreen'));
|
||||
const TopUpScreen = lazy(() => import('./pages/TopUpScreen'));
|
||||
const PreferencesScreen = lazy(() => import('./pages/PreferencesScreen'));
|
||||
const NotFoundScreen = lazy(() => import('./pages/NotFoundScreen'));
|
||||
|
||||
const LoadingFallback = () => (
|
||||
<div className="container" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
|
||||
<div className="loading-spinner">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<FoodProvider>
|
||||
<CartProvider>
|
||||
<StockAlert />
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginScreen />} />
|
||||
<StockAlert />
|
||||
<MotionConfig skipAnimations={true}>
|
||||
<Router>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginScreen />} />
|
||||
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<HomeScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/orders" element={
|
||||
<ProtectedRoute>
|
||||
<MyOrdersScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/category/:categoryId" element={
|
||||
<ProtectedRoute>
|
||||
<CategoryScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/item/:itemId" element={
|
||||
<ProtectedRoute>
|
||||
<ItemDetailScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/stall/:id" element={
|
||||
<ProtectedRoute>
|
||||
<StallDetailScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<HomeScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/orders" element={
|
||||
<ProtectedRoute>
|
||||
<MyOrdersScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/category/:categoryId" element={
|
||||
<ProtectedRoute>
|
||||
<CategoryScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/item/:itemId" element={
|
||||
<ProtectedRoute>
|
||||
<ItemDetailScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/stall/:id" element={
|
||||
<ProtectedRoute>
|
||||
<StallDetailScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/cart" element={
|
||||
<ProtectedRoute>
|
||||
<CartScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/checkout" element={
|
||||
<ProtectedRoute>
|
||||
<CheckoutScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/success" element={
|
||||
<ProtectedRoute>
|
||||
<SuccessScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/profile" element={
|
||||
<ProtectedRoute>
|
||||
<ProfileScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/change-pin" element={
|
||||
<ProtectedRoute>
|
||||
<ChangePinScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/wallet" element={
|
||||
<ProtectedRoute>
|
||||
<WalletScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/topup" element={
|
||||
<ProtectedRoute>
|
||||
<TopUpScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/preferences" element={
|
||||
<ProtectedRoute>
|
||||
<PreferencesScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="*" element={<NotFoundScreen />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</CartProvider>
|
||||
</FoodProvider>
|
||||
</AuthProvider>
|
||||
<Route path="/cart" element={
|
||||
<ProtectedRoute>
|
||||
<CartScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/checkout" element={
|
||||
<ProtectedRoute>
|
||||
<CheckoutScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/success" element={
|
||||
<ProtectedRoute>
|
||||
<SuccessScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/profile" element={
|
||||
<ProtectedRoute>
|
||||
<ProfileScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/change-pin" element={
|
||||
<ProtectedRoute>
|
||||
<ChangePinScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/wallet" element={
|
||||
<ProtectedRoute>
|
||||
<WalletScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/topup" element={
|
||||
<ProtectedRoute>
|
||||
<TopUpScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/preferences" element={
|
||||
<ProtectedRoute>
|
||||
<PreferencesScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="*" element={<NotFoundScreen />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Router>
|
||||
</MotionConfig>
|
||||
</CartProvider>
|
||||
</FoodProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 371 KiB |
BIN
ordering_site/src/assets/RIT_Logo.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
ordering_site/src/assets/Tillo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
ordering_site/src/assets/display_ritz.webp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
ordering_site/src/assets/front.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
ordering_site/src/assets/tillo_spinner.gif
Normal file
|
After Width: | Height: | Size: 41 KiB |
68
ordering_site/src/components/StockAlert.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { AlertCircle, X } from 'lucide-react';
|
||||
import { useCart } from '../contexts/CartContext';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
const StockAlert: React.FC = () => {
|
||||
const { stockError, clearStockError } = useCart();
|
||||
|
||||
useEffect(() => {
|
||||
if (stockError) {
|
||||
const timer = setTimeout(() => {
|
||||
clearStockError();
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [stockError, clearStockError]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{stockError && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -50, scale: 0.9 }}
|
||||
animate={{ opacity: 1, y: 16, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 9999,
|
||||
width: 'calc(100% - 32px)',
|
||||
maxWidth: '400px',
|
||||
backgroundColor: '#FF4D4F',
|
||||
color: '#ffffff',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 8px 24px rgba(255, 77, 79, 0.25)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '12px'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<AlertCircle size={20} style={{ flexShrink: 0 }} />
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 600 }}>{stockError}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={clearStockError}
|
||||
style={{
|
||||
border: 'none',
|
||||
color: '#ffffff',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
padding: '4px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)'
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockAlert;
|
||||
174
ordering_site/src/components/StockConflictModal.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
import { AlertTriangle, Trash2, Edit2, X } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface StockConflict {
|
||||
productId: number;
|
||||
productName: string;
|
||||
requestedQty: number;
|
||||
availableQty: number;
|
||||
}
|
||||
|
||||
interface StockConflictModalProps {
|
||||
conflicts: StockConflict[];
|
||||
onRemoveItem: (productId: number) => void;
|
||||
onAdjustQuantity: (productId: number, newQty: number) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StockConflictModal: React.FC<StockConflictModalProps> = ({
|
||||
conflicts,
|
||||
onRemoveItem,
|
||||
onAdjustQuantity,
|
||||
onClose,
|
||||
}) => {
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 10000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '16px',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
backdropFilter: 'blur(4px)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Modal Container */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
maxWidth: '450px',
|
||||
backgroundColor: 'var(--surface)',
|
||||
borderRadius: '24px',
|
||||
boxShadow: 'var(--shadow)',
|
||||
border: '1px solid var(--border)',
|
||||
overflow: 'hidden',
|
||||
padding: '24px'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#ff4d4f' }}>
|
||||
<AlertTriangle size={24} />
|
||||
<h3 style={{ fontSize: '1.2rem', fontWeight: 800 }}>Stock Issues Detected</h3>
|
||||
</div>
|
||||
<button onClick={onClose} style={{ cursor: 'pointer', border: 'none', background: 'none', color: 'var(--text-light)' }}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p style={{ color: 'var(--text-mid)', fontSize: '0.9rem', marginBottom: '20px' }}>
|
||||
Some items in your cart do not have enough stock available to complete the order:
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', marginBottom: '24px' }}>
|
||||
{conflicts.map((conflict) => (
|
||||
<div
|
||||
key={conflict.productId}
|
||||
style={{
|
||||
padding: '16px',
|
||||
borderRadius: '16px',
|
||||
backgroundColor: 'var(--input-bg)',
|
||||
border: '1px solid var(--border)'
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 700, fontSize: '0.95rem', color: 'var(--text-dark)', marginBottom: '8px' }}>
|
||||
{conflict.productName}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.85rem', color: 'var(--text-light)', marginBottom: '16px' }}>
|
||||
<span>Requested: {conflict.requestedQty}</span>
|
||||
<span style={{ color: '#ff4d4f', fontWeight: 600 }}>{conflict.availableQty > 0 ? 'In Stock (Limited)' : 'Out of Stock'}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||
{conflict.availableQty > 0 ? (
|
||||
<button
|
||||
onClick={() => onAdjustQuantity(conflict.productId, conflict.availableQty)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px',
|
||||
padding: '10px',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: 'var(--primary-light)',
|
||||
color: 'var(--primary)',
|
||||
border: 'none',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.85rem',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
<span>Reduce Quantity</span>
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ color: '#ff4d4f', fontSize: '0.85rem', fontWeight: 700, display: 'flex', alignItems: 'center' }}>
|
||||
Out of Stock
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => onRemoveItem(conflict.productId)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px',
|
||||
padding: '10px',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: '#FFF1F2',
|
||||
color: '#FF4D4F',
|
||||
border: 'none',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.85rem',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>Remove Item</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'var(--primary)',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
fontWeight: 800,
|
||||
fontSize: '0.95rem',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Return to Cart
|
||||
</button>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockConflictModal;
|
||||
48
ordering_site/src/contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [theme, setThemeState] = useState<Theme>(() => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'light' || savedTheme === 'dark') {
|
||||
return savedTheme;
|
||||
}
|
||||
return 'light';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setThemeState((prev) => (prev === 'light' ? 'dark' : 'light'));
|
||||
};
|
||||
|
||||
const setTheme = (newTheme: Theme) => {
|
||||
setThemeState(newTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
16
ordering_site/src/firebase.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { getAuth, GoogleAuthProvider } from "firebase/auth";
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDOEiKtH-gs2nAx8Di45wJf8CY5nPm4xPE",
|
||||
authDomain: "tillo-c8de9.firebaseapp.com",
|
||||
projectId: "tillo-c8de9",
|
||||
storageBucket: "tillo-c8de9.firebasestorage.app",
|
||||
messagingSenderId: "200333262782",
|
||||
appId: "1:200333262782:web:0d988352ee6a44d7a943ef",
|
||||
measurementId: "G-YB01EQT2VW"
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
export const auth = getAuth(app);
|
||||
export const googleProvider = new GoogleAuthProvider();
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
PlusCircle,
|
||||
CreditCard
|
||||
} from 'lucide-react';
|
||||
import tokenImage from '../assets/display_ritz.png';
|
||||
import tokenImage from '../assets/display_ritz.webp';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import Header from '../components/Header';
|
||||
import StockConflictModal from '../components/StockConflictModal';
|
||||
@@ -16,6 +16,7 @@ import { useCart } from '../contexts/CartContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useFood } from '../contexts/FoodContext';
|
||||
import { payAndVerify, authHeaders } from '../utils/razorpay';
|
||||
import { getApiBaseUrl } from '../utils/api';
|
||||
import './CheckoutScreen.css';
|
||||
|
||||
type PaymentMethod = 'RITZ_TOKEN' | 'RAZORPAY';
|
||||
@@ -50,7 +51,7 @@ const CheckoutScreen: React.FC = () => {
|
||||
const fetchBalance = async () => {
|
||||
if (!user) return;
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/wallet/balance/${user.id}`, {
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/wallet/balance/${user.id}`, {
|
||||
headers: authHeaders()
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -101,7 +102,7 @@ const CheckoutScreen: React.FC = () => {
|
||||
user: { id: user.id }
|
||||
};
|
||||
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders`, {
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/orders`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(orderData),
|
||||
|
||||
@@ -9,7 +9,8 @@ import FeedbackModal from '../components/FeedbackModal';
|
||||
import { useFood } from '../contexts/FoodContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import walletIcon from '../assets/front.png';
|
||||
import walletIcon from '../assets/front.webp';
|
||||
import { getApiBaseUrl } from '../utils/api';
|
||||
import './HomeScreen.css';
|
||||
|
||||
const HomeScreen: React.FC = () => {
|
||||
@@ -36,7 +37,7 @@ const HomeScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/latest-unrated/${user?.id}`, {
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/feedback/latest-unrated/${user?.id}`, {
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
}
|
||||
@@ -59,7 +60,7 @@ const HomeScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
await fetch(`http://${window.location.hostname}:8080/api/feedback/skip/${unratedOrder.id}`, {
|
||||
await fetch(`${getApiBaseUrl()}/api/feedback/skip/${unratedOrder.id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
@@ -80,7 +81,7 @@ const HomeScreen: React.FC = () => {
|
||||
const handleFeedbackSubmit = async (feedbackData: any) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/submit`, {
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/feedback/submit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -146,7 +147,7 @@ const HomeScreen: React.FC = () => {
|
||||
const fetchSearchResults = async () => {
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/products?search=${encodeURIComponent(debouncedSearch)}&size=100`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/products?search=${encodeURIComponent(debouncedSearch)}&size=100`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const products = data.content || data || [];
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useFood } from '../contexts/FoodContext';
|
||||
import CartTab from '../components/CartTab';
|
||||
import { VegNonVegIcon } from '../components/ItemCard';
|
||||
import './ItemDetailScreen.css';
|
||||
import { getApiBaseUrl } from '../utils/api';
|
||||
|
||||
const ItemDetailScreen: React.FC = () => {
|
||||
const { itemId } = useParams<{ itemId: string }>();
|
||||
@@ -32,7 +33,7 @@ const ItemDetailScreen: React.FC = () => {
|
||||
const fetchItem = async () => {
|
||||
setIsFetching(true);
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/products/${itemId}`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/products/${itemId}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
@@ -47,7 +48,7 @@ const ItemDetailScreen: React.FC = () => {
|
||||
let finalRatingCount = 0;
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const statsRes = await fetch(`http://${window.location.hostname}:8080/api/feedback/item-stats?productName=${encodeURIComponent(data.name)}`, {
|
||||
const statsRes = await fetch(`${getApiBaseUrl()}/api/feedback/item-stats?productName=${encodeURIComponent(data.name)}`, {
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
});
|
||||
if (statsRes.ok) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Lock } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import tilloLogo from '../assets/Tillo.png';
|
||||
import ritLogo from '../assets/RIT_Logo.png';
|
||||
import ritLogo from '../assets/RIT_Logo.webp';
|
||||
import { auth, googleProvider } from '../firebase';
|
||||
import { signInWithPopup } from 'firebase/auth';
|
||||
import './LoginScreen.css';
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useFood } from '../contexts/FoodContext';
|
||||
import { useCart } from '../contexts/CartContext';
|
||||
import type { FoodItem } from '../types';
|
||||
import './MyOrdersScreen.css';
|
||||
import { getApiBaseUrl } from '../utils/api';
|
||||
|
||||
interface Order {
|
||||
id: number;
|
||||
@@ -82,7 +83,7 @@ const MyOrdersScreen: React.FC = () => {
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/user/${user?.id}`, {
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/orders/user/${user?.id}`, {
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
}
|
||||
@@ -143,7 +144,7 @@ const MyOrdersScreen: React.FC = () => {
|
||||
setIsCancelling(true);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/cancel`, {
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/orders/${orderId}/cancel`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
|
||||
46
ordering_site/src/pages/NotFoundScreen.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AlertTriangle, Home } from 'lucide-react';
|
||||
|
||||
const NotFoundScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', padding: '20px', textAlign: 'center' }}>
|
||||
<div style={{ width: '80px', height: '80px', borderRadius: '50%', backgroundColor: 'var(--primary-light)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--primary)', marginBottom: '24px' }}>
|
||||
<AlertTriangle size={40} />
|
||||
</div>
|
||||
|
||||
<h1 style={{ fontSize: '2rem', fontWeight: 900, color: 'var(--text-dark)', marginBottom: '12px' }}>
|
||||
Page Not Found
|
||||
</h1>
|
||||
|
||||
<p style={{ color: 'var(--text-light)', fontSize: '0.95rem', maxWidth: '320px', marginBottom: '32px' }}>
|
||||
The page you are looking for doesn't exist or has been moved to another URL.
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
style={{
|
||||
backgroundColor: 'var(--primary)',
|
||||
color: '#ffffff',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '12px',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.95rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 4px 12px rgba(35, 22, 81, 0.15)'
|
||||
}}
|
||||
>
|
||||
<Home size={18} />
|
||||
<span>Go to Home</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFoundScreen;
|
||||
79
ordering_site/src/pages/PreferencesScreen.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import Header from '../components/Header';
|
||||
import BottomNav from '../components/BottomNav';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { Moon, Sun, Globe, ChevronRight } from 'lucide-react';
|
||||
|
||||
const PreferencesScreen: React.FC = () => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="container preferences-page">
|
||||
<Header title="Preferences" showCart={false} />
|
||||
<main className="safe-area-bottom">
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div
|
||||
className="preference-item"
|
||||
onClick={toggleTheme}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
backgroundColor: 'var(--surface)',
|
||||
borderRadius: '12px',
|
||||
marginBottom: '16px',
|
||||
cursor: 'pointer',
|
||||
boxShadow: 'var(--shadow)'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
{theme === 'dark' ? <Moon size={20} color="var(--primary)" /> : <Sun size={20} color="var(--primary)" />}
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, color: 'var(--text-dark)' }}>Dark Mode</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-light)' }}>
|
||||
{theme === 'dark' ? 'Enabled' : 'Disabled'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '0.9rem', color: 'var(--text-light)', marginRight: '8px' }}>
|
||||
{theme === 'dark' ? 'On' : 'Off'}
|
||||
</span>
|
||||
<ChevronRight size={18} color="var(--text-light)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="preference-item"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
backgroundColor: 'var(--surface)',
|
||||
borderRadius: '12px',
|
||||
marginBottom: '16px',
|
||||
boxShadow: 'var(--shadow)'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Globe size={20} color="var(--primary)" />
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, color: 'var(--text-dark)' }}>Language</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-light)' }}>Change application language</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '0.9rem', color: 'var(--text-light)', marginRight: '8px' }}>English</span>
|
||||
<ChevronRight size={18} color="var(--text-light)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreferencesScreen;
|
||||
@@ -6,6 +6,7 @@ import CartTab from '../components/CartTab';
|
||||
import BottomNav from '../components/BottomNav';
|
||||
import type { Stall } from '../types';
|
||||
import './StallDetailScreen.css';
|
||||
import { getApiBaseUrl } from '../utils/api';
|
||||
|
||||
const StallDetailScreen: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -18,7 +19,7 @@ const StallDetailScreen: React.FC = () => {
|
||||
const fetchStallDetails = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/stalls/${id}`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/stalls/${id}`);
|
||||
if (!response.ok) throw new Error('Stall not found');
|
||||
const data = await response.json();
|
||||
|
||||
@@ -61,8 +62,7 @@ const StallDetailScreen: React.FC = () => {
|
||||
fetchStallDetails();
|
||||
|
||||
// SSE Real-time stock updates
|
||||
const host = window.location.hostname;
|
||||
const eventSource = new EventSource(`http://${host}:8080/api/stock/stream`);
|
||||
const eventSource = new EventSource(`${getApiBaseUrl()}/api/stock/stream`);
|
||||
|
||||
eventSource.addEventListener('stockUpdate', (event: any) => {
|
||||
try {
|
||||
|
||||
358
ordering_site/src/pages/TopUpScreen.css
Normal file
@@ -0,0 +1,358 @@
|
||||
/* ═══════════════════════════════════════════════
|
||||
TopUp Screen — Premium Razorpay Recharge UI
|
||||
═══════════════════════════════════════════════ */
|
||||
|
||||
.topup-page main {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
/* ── Back Button ─────────────────────────────── */
|
||||
.topup-back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-mid);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.topup-back-btn:hover {
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
/* ── Balance Card ────────────────────────────── */
|
||||
.topup-balance-card {
|
||||
background: linear-gradient(135deg, #001828 0%, #4a3e7a 100%);
|
||||
color: #ffffff;
|
||||
padding: 24px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 32px rgba(35, 22, 81, 0.18);
|
||||
}
|
||||
|
||||
.topup-balance-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
right: -30px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.topup-balance-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: -20px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 2rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
/* ── Razorpay Info Banner ────────────────────── */
|
||||
.razorpay-info-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: rgba(var(--primary-rgb), 0.08);
|
||||
border: 1px solid rgba(var(--primary-rgb), 0.2);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.razorpay-info-banner .banner-icon {
|
||||
color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.razorpay-info-banner .banner-text {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-mid);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.razorpay-info-banner .banner-text strong {
|
||||
color: var(--text-dark);
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* ── Form Styles ─────────────────────────────── */
|
||||
.topup-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.topup-form label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-mid);
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.amount-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.amount-input-wrapper .currency-symbol {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.amount-input-wrapper input {
|
||||
width: 100%;
|
||||
padding: 16px 16px 16px 40px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
border-radius: 16px;
|
||||
border: 2px solid var(--border);
|
||||
background-color: var(--surface);
|
||||
color: var(--text-dark);
|
||||
outline: none;
|
||||
transition: border-color 0.25s, box-shadow 0.25s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.amount-input-wrapper input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 4px rgba(var(--primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
.amount-input-wrapper input::placeholder {
|
||||
color: var(--text-light);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Quick Presets ────────────────────────────── */
|
||||
.presets-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.presets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
padding: 12px 6px;
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid var(--border);
|
||||
background-color: var(--surface);
|
||||
color: var(--text-dark);
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.preset-btn:hover {
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.preset-btn.active {
|
||||
background-color: var(--primary);
|
||||
color: #ffffff;
|
||||
border-color: var(--primary);
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.25);
|
||||
}
|
||||
|
||||
/* ── Error Message ───────────────────────────── */
|
||||
.topup-error {
|
||||
color: #FF4D4F;
|
||||
background-color: #FFF1F2;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid #fecaca;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
animation: topup-error-shake 0.35s ease;
|
||||
}
|
||||
|
||||
@keyframes topup-error-shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-4px); }
|
||||
40% { transform: translateX(4px); }
|
||||
60% { transform: translateX(-3px); }
|
||||
80% { transform: translateX(2px); }
|
||||
}
|
||||
|
||||
/* ── Pay Button ──────────────────────────────── */
|
||||
.topup-submit-btn {
|
||||
background-color: var(--primary);
|
||||
color: #ffffff;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
font-weight: 800;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.15s;
|
||||
margin-top: 10px;
|
||||
font-family: inherit;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topup-submit-btn:not(:disabled):hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(var(--primary-rgb), 0.3);
|
||||
}
|
||||
|
||||
.topup-submit-btn:not(:disabled):active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.topup-submit-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.topup-submit-btn.processing {
|
||||
background: linear-gradient(90deg, var(--primary), color-mix(in srgb, var(--primary) 70%, #000));
|
||||
}
|
||||
|
||||
/* ── Shimmer animation for processing state ── */
|
||||
.topup-submit-btn.processing::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.15) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
/* ── Security Footer ─────────────────────────── */
|
||||
.topup-security-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-light);
|
||||
font-size: 0.8rem;
|
||||
justify-content: center;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
/* ── Success Overlay ─────────────────────────── */
|
||||
.topup-success-overlay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.topup-success-icon {
|
||||
animation: topup-success-pop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@keyframes topup-success-pop {
|
||||
0% { transform: scale(0); opacity: 0; }
|
||||
60% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.topup-success-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-dark);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.topup-success-amount {
|
||||
color: var(--text-light);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.topup-success-redirect {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
animation: topup-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes topup-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* ── Conversion hint ─────────────────────────── */
|
||||
.conversion-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-light);
|
||||
margin-top: 4px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.conversion-hint .equals-sign {
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
195
ordering_site/src/pages/TopUpScreen.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Wallet, ShieldCheck, CheckCircle, CreditCard, Zap } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import Header from '../components/Header';
|
||||
import { payAndVerify } from '../utils/razorpay';
|
||||
import './TopUpScreen.css';
|
||||
|
||||
const TopUpScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, refreshUser } = useAuth();
|
||||
const [amount, setAmount] = useState<string>('');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [creditedAmount, setCreditedAmount] = useState<number>(0);
|
||||
|
||||
const presets = [50, 100, 200, 500];
|
||||
|
||||
const handleTopUp = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user) return;
|
||||
|
||||
const parsedAmount = parseFloat(amount);
|
||||
if (isNaN(parsedAmount) || parsedAmount < 50) {
|
||||
setError('Minimum top up amount is ₹50.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedAmount > 5000) {
|
||||
setError('Maximum top up limit per transaction is ₹5,000.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(parsedAmount)) {
|
||||
setError('Please enter a whole number amount (no paise).');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await payAndVerify({
|
||||
purpose: 'WALLET_TOPUP',
|
||||
amount: parsedAmount
|
||||
});
|
||||
|
||||
if (result.success && (result.type === 'WALLET_TOPUP' || result.purpose === 'WALLET_TOPUP')) {
|
||||
setCreditedAmount(parsedAmount);
|
||||
setSuccess(true);
|
||||
await refreshUser();
|
||||
setTimeout(() => {
|
||||
navigate('/wallet');
|
||||
}, 2200);
|
||||
} else {
|
||||
setError(result.message || 'Payment succeeded but wallet was not credited. Contact support with your payment ID.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Top up error:', err);
|
||||
if (err?.message === 'Payment cancelled') {
|
||||
setError('Payment was cancelled. No tokens were added.');
|
||||
} else {
|
||||
setError(err?.message || 'Connection error. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="container topup-page">
|
||||
<div className="topup-success-overlay">
|
||||
<div className="topup-success-icon">
|
||||
<CheckCircle size={64} color="var(--primary)" />
|
||||
</div>
|
||||
<h2 className="topup-success-title">
|
||||
Top Up Successful!
|
||||
</h2>
|
||||
<p className="topup-success-amount">
|
||||
🅡{creditedAmount.toLocaleString()} Ritz tokens have been added to your wallet.
|
||||
</p>
|
||||
<div className="topup-success-redirect">
|
||||
Redirecting to your wallet...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container topup-page">
|
||||
<Header title="Top Up Tokens" showCart={false} />
|
||||
|
||||
<main style={{ padding: '20px' }}>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="topup-back-btn"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
|
||||
{/* Balance Card */}
|
||||
<div className="topup-balance-card">
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||
<div className="balance-label">Current Balance</div>
|
||||
<Wallet size={20} style={{ opacity: 0.7 }} />
|
||||
</div>
|
||||
<div className="balance-amount">🅡{(user?.ritzTokenBalance || 0).toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
{/* Razorpay Info */}
|
||||
<div className="razorpay-info-banner">
|
||||
<CreditCard size={20} className="banner-icon" />
|
||||
<div className="banner-text">
|
||||
<strong>Pay securely with Razorpay</strong>
|
||||
UPI, cards & netbanking. Tokens are credited only after payment succeeds.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleTopUp} className="topup-form">
|
||||
<div>
|
||||
<label>Enter Top Up Amount (₹)</label>
|
||||
<div className="amount-input-wrapper">
|
||||
<span className="currency-symbol">₹</span>
|
||||
<input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => {
|
||||
setAmount(e.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
placeholder="0"
|
||||
min="50"
|
||||
max="5000"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{amount && Number(amount) > 0 && (
|
||||
<div className="conversion-hint">
|
||||
<Zap size={12} />
|
||||
<span>₹{Number(amount).toLocaleString()}</span>
|
||||
<span className="equals-sign">=</span>
|
||||
<span>🅡{Number(amount).toLocaleString()} Ritz Tokens</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="presets-label">Quick Add Presets</span>
|
||||
<div className="presets-grid">
|
||||
{presets.map((preset) => (
|
||||
<button
|
||||
key={preset}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAmount(preset.toString());
|
||||
setError(null);
|
||||
}}
|
||||
className={`preset-btn ${amount === preset.toString() ? 'active' : ''}`}
|
||||
>
|
||||
+₹{preset}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="topup-error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isProcessing || !amount}
|
||||
className={`topup-submit-btn ${isProcessing ? 'processing' : ''}`}
|
||||
>
|
||||
{isProcessing ? 'Processing Payment...' : `Pay ₹${amount || '0'} & Top Up`}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="topup-security-footer">
|
||||
<ShieldCheck size={14} />
|
||||
<span>Secured by Razorpay · 1 Ritz Token = ₹1.00</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopUpScreen;
|
||||
322
ordering_site/src/pages/WalletScreen.css
Normal file
@@ -0,0 +1,322 @@
|
||||
/* ═══════════════════════════════════════════════
|
||||
Wallet Screen — Premium Ritz Wallet UI
|
||||
═══════════════════════════════════════════════ */
|
||||
|
||||
.wallet-page main {
|
||||
padding: 20px;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
/* ── Gradient Balance Card ───────────────────── */
|
||||
.wallet-balance-card {
|
||||
background: linear-gradient(135deg, #001828 0%, #4a3e7a 100%);
|
||||
color: #ffffff;
|
||||
padding: 24px;
|
||||
borderRadius: 24px;
|
||||
box-shadow: 0 8px 32px rgba(35, 22, 81, 0.15);
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.wallet-balance-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: -40px;
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wallet-balance-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
left: 30px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wallet-balance-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.wallet-balance-label {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.wallet-balance-icon {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.wallet-balance-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.wallet-token-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.wallet-balance-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
/* ── Top Up Button (inside balance card) ─────── */
|
||||
.wallet-topup-btn {
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
color: #001828;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.wallet-topup-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.wallet-topup-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* ── Coupon Card ─────────────────────────────── */
|
||||
.wallet-coupon-card {
|
||||
background-color: var(--surface);
|
||||
padding: 20px;
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.wallet-coupon-card h4 {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-dark);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.wallet-coupon-card p {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.coupon-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.coupon-input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.coupon-input {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--input-bg);
|
||||
color: var(--text-dark);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
outline: none;
|
||||
text-transform: uppercase;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.coupon-input:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.coupon-apply-btn {
|
||||
background-color: var(--primary);
|
||||
color: #ffffff;
|
||||
padding: 0 20px;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, opacity 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.coupon-apply-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.coupon-apply-btn:not(:disabled):active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.coupon-status {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
animation: coupon-status-slide 0.3s ease;
|
||||
}
|
||||
|
||||
.coupon-status.success {
|
||||
background-color: #f0fdf4;
|
||||
color: #166534;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.coupon-status.error {
|
||||
background-color: #fef2f2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
@keyframes coupon-status-slide {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Transactions Section ────────────────────── */
|
||||
.wallet-transactions-header {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.wallet-transactions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wallet-tx-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
background-color: var(--surface);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.wallet-tx-card:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.wallet-tx-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wallet-tx-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wallet-tx-icon.credit {
|
||||
background-color: #E6FFFA;
|
||||
color: #00B894;
|
||||
}
|
||||
|
||||
.wallet-tx-icon.debit {
|
||||
background-color: #FFF1F2;
|
||||
color: #FF4D4F;
|
||||
}
|
||||
|
||||
.wallet-tx-description {
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.wallet-tx-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-light);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.wallet-tx-ref {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.wallet-tx-amount {
|
||||
font-weight: 800;
|
||||
font-size: 1.05rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wallet-tx-amount.credit {
|
||||
color: #00B894;
|
||||
}
|
||||
|
||||
.wallet-tx-amount.debit {
|
||||
color: #FF4D4F;
|
||||
}
|
||||
|
||||
.wallet-empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background-color: var(--surface);
|
||||
border-radius: 16px;
|
||||
color: var(--text-light);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.wallet-loading-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text-light);
|
||||
}
|
||||
222
ordering_site/src/pages/WalletScreen.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Wallet, PlusCircle, ArrowUpRight, ArrowDownLeft, Calendar } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { authHeaders } from '../utils/razorpay';
|
||||
import Header from '../components/Header';
|
||||
import BottomNav from '../components/BottomNav';
|
||||
import tokenImage from '../assets/display_ritz.webp';
|
||||
import './WalletScreen.css';
|
||||
import { getApiBaseUrl } from '../utils/api';
|
||||
|
||||
interface Transaction {
|
||||
id: number;
|
||||
amount: number;
|
||||
type: 'TOPUP' | 'SPEND' | 'REFUND';
|
||||
description: string;
|
||||
timestamp: string;
|
||||
referenceId: string;
|
||||
}
|
||||
|
||||
const WalletScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const [balance, setBalance] = useState<number>(user?.ritzTokenBalance || 0);
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Coupon States
|
||||
const [couponCode, setCouponCode] = useState('');
|
||||
const [redeemStatus, setRedeemStatus] = useState<{ type: 'success' | 'error' | null; message: string }>({ type: null, message: '' });
|
||||
const [isRedeeming, setIsRedeeming] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetchWalletData();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchWalletData = async () => {
|
||||
if (!user) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// Fetch balance
|
||||
const balanceRes = await fetch(`${getApiBaseUrl()}/api/wallet/balance/${user.id}`, {
|
||||
headers: authHeaders()
|
||||
});
|
||||
if (balanceRes.ok) {
|
||||
const balanceData = await balanceRes.json();
|
||||
setBalance(balanceData.balance || 0);
|
||||
}
|
||||
|
||||
// Fetch transactions
|
||||
const txRes = await fetch(`${getApiBaseUrl()}/api/wallet/transactions/${user.id}`, {
|
||||
headers: authHeaders()
|
||||
});
|
||||
if (txRes.ok) {
|
||||
const txData = await txRes.json();
|
||||
setTransactions(txData || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching wallet data:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRedeemCoupon = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!couponCode.trim()) return;
|
||||
|
||||
try {
|
||||
setIsRedeeming(true);
|
||||
setRedeemStatus({ type: null, message: '' });
|
||||
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/coupons/redeem`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
code: couponCode.trim(),
|
||||
userId: user?.id
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok && data.success) {
|
||||
setRedeemStatus({
|
||||
type: 'success',
|
||||
message: data.message || `Successfully redeemed coupon code!`
|
||||
});
|
||||
setCouponCode('');
|
||||
// Refresh balance and transactions
|
||||
fetchWalletData();
|
||||
} else {
|
||||
setRedeemStatus({
|
||||
type: 'error',
|
||||
message: data.message || 'Failed to redeem coupon.'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error redeeming coupon:', error);
|
||||
setRedeemStatus({
|
||||
type: 'error',
|
||||
message: 'Something went wrong. Please try again.'
|
||||
});
|
||||
} finally {
|
||||
setIsRedeeming(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container wallet-page">
|
||||
<Header title="My Wallet" showCart={false} />
|
||||
|
||||
<main className="safe-area-bottom" style={{ padding: '20px' }}>
|
||||
{/* Balance Card */}
|
||||
<div className="wallet-balance-card">
|
||||
<div className="wallet-balance-header">
|
||||
<span className="wallet-balance-label">Ritz Token Balance</span>
|
||||
<Wallet size={20} className="wallet-balance-icon" />
|
||||
</div>
|
||||
|
||||
<div className="wallet-balance-display">
|
||||
<img src={tokenImage} alt="Ritz Token" className="wallet-token-image" />
|
||||
<span className="wallet-balance-value">
|
||||
🅡{balance.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/topup')}
|
||||
className="wallet-topup-btn"
|
||||
>
|
||||
<PlusCircle size={18} />
|
||||
<span>Top Up Tokens via Razorpay</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Redeem Coupon Card */}
|
||||
<div className="wallet-coupon-card">
|
||||
<h4>Redeem Coupon Code</h4>
|
||||
<p>Claim promo codes or reward vouchers to instantly credit tokens to your wallet.</p>
|
||||
|
||||
<form onSubmit={handleRedeemCoupon} className="coupon-form">
|
||||
<div className="coupon-input-row">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter Coupon Code"
|
||||
value={couponCode}
|
||||
onChange={(e) => setCouponCode(e.target.value)}
|
||||
disabled={isRedeeming}
|
||||
className="coupon-input"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isRedeeming || !couponCode.trim()}
|
||||
className="coupon-apply-btn"
|
||||
>
|
||||
{isRedeeming ? 'Redeeming...' : 'Apply'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{redeemStatus.type && (
|
||||
<div className={`coupon-status ${redeemStatus.type}`}>
|
||||
{redeemStatus.message}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Transactions Section */}
|
||||
<div>
|
||||
<h3 className="wallet-transactions-header">
|
||||
Transaction History
|
||||
</h3>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="wallet-loading-state">
|
||||
Loading transactions...
|
||||
</div>
|
||||
) : transactions.length === 0 ? (
|
||||
<div className="wallet-empty-state">
|
||||
No transactions yet. Top up your wallet to get started!
|
||||
</div>
|
||||
) : (
|
||||
<div className="wallet-transactions-list">
|
||||
{transactions.map((tx) => {
|
||||
const isCredit = tx.type === 'TOPUP' || tx.type === 'REFUND';
|
||||
return (
|
||||
<div key={tx.id} className="wallet-tx-card">
|
||||
<div className="wallet-tx-left">
|
||||
<div className={`wallet-tx-icon ${isCredit ? 'credit' : 'debit'}`}>
|
||||
{isCredit ? <ArrowDownLeft size={20} /> : <ArrowUpRight size={20} />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="wallet-tx-description">
|
||||
{tx.description || (tx.type === 'TOPUP' ? 'Wallet Top Up' : tx.type === 'REFUND' ? 'Refund' : 'Order Payment')}
|
||||
</div>
|
||||
<div className="wallet-tx-meta">
|
||||
<Calendar size={12} />
|
||||
<span>{new Date(tx.timestamp).toLocaleString()}</span>
|
||||
{tx.referenceId && <span className="wallet-tx-ref">• {tx.referenceId}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`wallet-tx-amount ${isCredit ? 'credit' : 'debit'}`}>
|
||||
{isCredit ? '+' : '-'}🅡{tx.amount.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WalletScreen;
|
||||
6
ordering_site/src/utils/api.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const getApiBaseUrl = (): string => {
|
||||
if (window.location.protocol === 'https:') {
|
||||
return 'https://cmsapi.rit-services.in';
|
||||
}
|
||||
return `http://${window.location.hostname}:8080`;
|
||||
};
|
||||
194
ordering_site/src/utils/razorpay.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Razorpay Checkout helpers for the ordering site.
|
||||
* Secret key never touches the browser — only key_id + order_id from the backend.
|
||||
*/
|
||||
|
||||
export type RazorpayPrefill = {
|
||||
name?: string;
|
||||
email?: string;
|
||||
contact?: string;
|
||||
};
|
||||
|
||||
export type CreatePaymentResponse = {
|
||||
success: boolean;
|
||||
paymentSessionId: number;
|
||||
keyId: string;
|
||||
razorpayOrderId: string;
|
||||
amount: number; // paise
|
||||
amountInr: number;
|
||||
currency: string;
|
||||
purpose: string;
|
||||
name: string;
|
||||
description: string;
|
||||
prefill?: RazorpayPrefill;
|
||||
notes?: Record<string, string>;
|
||||
message?: string;
|
||||
errorType?: string;
|
||||
conflicts?: unknown[];
|
||||
};
|
||||
|
||||
export type VerifyPaymentResponse = {
|
||||
success: boolean;
|
||||
type?: string;
|
||||
purpose?: string;
|
||||
status?: string;
|
||||
orderNumber?: string;
|
||||
displayOrderId?: string;
|
||||
newBalance?: number;
|
||||
message?: string;
|
||||
creditedAmount?: number;
|
||||
razorpayPaymentId?: string;
|
||||
};
|
||||
|
||||
export type RazorpaySuccessResponse = {
|
||||
razorpay_order_id: string;
|
||||
razorpay_payment_id: string;
|
||||
razorpay_signature: string;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Razorpay?: new (options: Record<string, unknown>) => {
|
||||
open: () => void;
|
||||
on: (event: string, handler: (response: unknown) => void) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
import { getApiBaseUrl } from './api';
|
||||
|
||||
const API_HOST = () => getApiBaseUrl();
|
||||
|
||||
export function authHeaders(): HeadersInit {
|
||||
const token = localStorage.getItem('token');
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
let razorpayScriptPromise: Promise<void> | null = null;
|
||||
|
||||
export function loadRazorpayScript(): Promise<void> {
|
||||
if (window.Razorpay) return Promise.resolve();
|
||||
if (razorpayScriptPromise) return razorpayScriptPromise;
|
||||
|
||||
razorpayScriptPromise = new Promise((resolve, reject) => {
|
||||
const existing = document.querySelector('script[data-razorpay="true"]');
|
||||
if (existing) {
|
||||
existing.addEventListener('load', () => resolve());
|
||||
existing.addEventListener('error', () => reject(new Error('Failed to load Razorpay SDK')));
|
||||
return;
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
|
||||
script.async = true;
|
||||
script.dataset.razorpay = 'true';
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => {
|
||||
razorpayScriptPromise = null;
|
||||
reject(new Error('Failed to load Razorpay Checkout. Check your network connection.'));
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
|
||||
return razorpayScriptPromise;
|
||||
}
|
||||
|
||||
export async function createPayment(body: Record<string, unknown>): Promise<CreatePaymentResponse> {
|
||||
const response = await fetch(`${API_HOST()}/api/payments/create`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok && data.success !== false) {
|
||||
throw new Error(data.message || 'Failed to create payment');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function verifyPayment(payload: {
|
||||
razorpayOrderId: string;
|
||||
razorpayPaymentId: string;
|
||||
razorpaySignature: string;
|
||||
}): Promise<VerifyPaymentResponse> {
|
||||
const response = await fetch(`${API_HOST()}/api/payments/verify`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || 'Payment verification failed');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens Razorpay Checkout and resolves only after the user completes payment.
|
||||
* Does NOT place the order — caller must call verifyPayment next.
|
||||
*/
|
||||
export async function openRazorpayCheckout(
|
||||
checkout: CreatePaymentResponse
|
||||
): Promise<RazorpaySuccessResponse> {
|
||||
await loadRazorpayScript();
|
||||
const RazorpayCtor = window.Razorpay;
|
||||
if (!RazorpayCtor) {
|
||||
throw new Error('Razorpay SDK is not available');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options: Record<string, unknown> = {
|
||||
key: checkout.keyId,
|
||||
amount: checkout.amount,
|
||||
currency: checkout.currency || 'INR',
|
||||
name: checkout.name || 'Tillo Canteen',
|
||||
description: checkout.description || 'Payment',
|
||||
order_id: checkout.razorpayOrderId,
|
||||
prefill: checkout.prefill || {},
|
||||
notes: checkout.notes || {},
|
||||
theme: { color: '#00B894' },
|
||||
modal: {
|
||||
ondismiss: () => {
|
||||
reject(new Error('Payment cancelled'));
|
||||
},
|
||||
},
|
||||
handler: (response: RazorpaySuccessResponse) => {
|
||||
resolve(response);
|
||||
},
|
||||
};
|
||||
|
||||
const rzp = new RazorpayCtor(options);
|
||||
rzp.on('payment.failed', (resp: unknown) => {
|
||||
const err = resp as { error?: { description?: string; reason?: string } };
|
||||
reject(new Error(err?.error?.description || err?.error?.reason || 'Payment failed'));
|
||||
});
|
||||
rzp.open();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Full pay → verify flow used by checkout and wallet top-up.
|
||||
*/
|
||||
export async function payAndVerify(
|
||||
createBody: Record<string, unknown>
|
||||
): Promise<VerifyPaymentResponse> {
|
||||
const created = await createPayment(createBody);
|
||||
if (!created.success) {
|
||||
const err = new Error(created.message || 'Could not start payment') as Error & {
|
||||
errorType?: string;
|
||||
conflicts?: unknown[];
|
||||
};
|
||||
err.errorType = created.errorType;
|
||||
err.conflicts = created.conflicts;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const payment = await openRazorpayCheckout(created);
|
||||
return verifyPayment({
|
||||
razorpayOrderId: payment.razorpay_order_id,
|
||||
razorpayPaymentId: payment.razorpay_payment_id,
|
||||
razorpaySignature: payment.razorpay_signature,
|
||||
});
|
||||
}
|
||||