Bug Fixes
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,5 +3,3 @@
|
|||||||
backend/razorpay.env
|
backend/razorpay.env
|
||||||
backend/src/main/resources/application-local.properties
|
backend/src/main/resources/application-local.properties
|
||||||
counter-frontend/.env
|
counter-frontend/.env
|
||||||
ordering-site/*
|
|
||||||
ordering_site/
|
|
||||||
|
|||||||
9
counter-frontend/dist/assets/AnimatePresence-CcsC7tO8.js
vendored
Normal file
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,9 +0,0 @@
|
|||||||
import{m as e,t,u as n}from"./index-Cw6Vy2ED.js";import{a as r,c as i,i as a,n as o,o as s,r as c,s as l}from"./proxy-D2DvKtgS.js";var u=t(),d=e(n(),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(a(t)&&e.isPresent&&!this.props.isPresent&&this.props.pop!==!1){let e=t.offsetParent,n=a(e)&&e.offsetWidth||0,r=a(e)&&e.offsetHeight||0,i=getComputedStyle(t),o=this.props.sizeRef.current;o.height=parseFloat(i.height),o.width=parseFloat(i.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),l=(0,d.useRef)({width:0,height:0,top:0,left:0,right:0,bottom:0}),{nonce:f}=(0,d.useContext)(c),p=m(s,e.props?.ref??e?.ref);return(0,d.useInsertionEffect)(()=>{let{width:e,height:c,top:u,left:d,right:p,bottom:m}=l.current;if(t||a===!1||!s.current||!e||!c)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: ${c}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:l,pop:a,children:a===!1?e:d.cloneElement(e,{ref:p})})}var _=({children:e,initial:t,isPresent:n,onExitComplete:i,custom:a,presenceAffectsLayout:o,mode:s,anchorX:c,anchorY:f,root:p})=>{let m=l(v),h=(0,d.useId)(),_=!0,y=(0,d.useMemo)(()=>(_=!1,{id:h,initial:t,isPresent:n,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))}),[n,m,i]);return o&&_&&(y={...y}),(0,d.useMemo)(()=>{m.forEach((e,t)=>m.set(t,!1))},[n]),d.useEffect(()=>{!n&&!m.size&&i&&i()},[n]),e=(0,u.jsx)(g,{pop:s===`popLayout`,isPresent:n,anchorX:c,anchorY:f,root:p,children:e}),(0,u.jsx)(r.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:e,custom:t,initial:n=!0,onExitComplete:r,presenceAffectsLayout:a=!0,mode:c=`sync`,propagate:f=!1,anchorX:p=`left`,anchorY:m=`top`,root:h})=>{let[g,v]=o(f),x=(0,d.useMemo)(()=>b(e),[e]),S=f&&!g?[]:x.map(y),C=(0,d.useRef)(!0),w=(0,d.useRef)(x),T=l(()=>new Map),E=(0,d.useRef)(new Set),[D,O]=(0,d.useState)(x),[k,A]=(0,d.useState)(x);s(()=>{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 c===`wait`&&j.length&&(e=j),A(b(e)),O(x),null}let{forceRender:M}=(0,d.useContext)(i);return(0,u.jsx)(u.Fragment,{children:k.map(e=>{let i=y(e),o=f&&!g?!1:x===k||S.includes(i);return(0,u.jsx)(_,{isPresent:o,initial:!C.current||n?void 0:!1,custom:t,presenceAffectsLayout:a,mode:c,root:h,onExitComplete:o?void 0:()=>{if(E.current.has(i))return;if(T.has(i))E.current.add(i),T.set(i,!0);else return;let e=!0;T.forEach(t=>{t||(e=!1)}),e&&(M?.(),A(w.current),f&&v?.(),r&&r())},anchorX:p,anchorY:m,children:e},i)})})};export{x as t};
|
|
||||||
File diff suppressed because one or more lines are too long
1
counter-frontend/dist/assets/Categories-DzWzJLW4.js
vendored
Normal file
1
counter-frontend/dist/assets/Categories-DzWzJLW4.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
71
counter-frontend/dist/assets/Orders-BqkxCVX3.js
vendored
Normal file
71
counter-frontend/dist/assets/Orders-BqkxCVX3.js
vendored
Normal file
File diff suppressed because one or more lines are too long
71
counter-frontend/dist/assets/Orders-DbYj06Fc.js
vendored
71
counter-frontend/dist/assets/Orders-DbYj06Fc.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
import{c as e,t,u as n}from"./index-Cw6Vy2ED.js";var r=e(`chevron-left`,[[`path`,{d:`m15 18-6-6 6-6`,key:`1wnfg3`}]]),i=e(`chevron-right`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]);n();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};
|
|
||||||
1
counter-frontend/dist/assets/Pagination-BYvNtzJC.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
5
counter-frontend/dist/assets/Products-Cs9hGefr.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12
counter-frontend/dist/assets/index-B-Yfb7Zz.js
vendored
Normal file
12
counter-frontend/dist/assets/index-B-Yfb7Zz.js
vendored
Normal file
File diff suppressed because one or more lines are too long
12
counter-frontend/dist/assets/index-Cw6Vy2ED.js
vendored
12
counter-frontend/dist/assets/index-Cw6Vy2ED.js
vendored
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
import{c as e}from"./index-Cw6Vy2ED.js";var t=e(`minus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}]]);export{t};
|
|
||||||
1
counter-frontend/dist/assets/minus-wD8f4ZO0.js
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 +0,0 @@
|
|||||||
import{c as e}from"./index-Cw6Vy2ED.js";var t=e(`plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]);export{t};
|
|
||||||
1
counter-frontend/dist/assets/plus-CtPxjhzP.js
vendored
Normal file
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};
|
||||||
File diff suppressed because one or more lines are too long
1
counter-frontend/dist/assets/proxy-cLldRXEc.js
vendored
Normal file
1
counter-frontend/dist/assets/proxy-cLldRXEc.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{c as e}from"./index-Cw6Vy2ED.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};
|
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 +1 @@
|
|||||||
import{c as e}from"./index-Cw6Vy2ED.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};
|
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};
|
||||||
2
counter-frontend/dist/index.html
vendored
2
counter-frontend/dist/index.html
vendored
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Counter POS</title>
|
<title>Counter POS</title>
|
||||||
<script type="module" crossorigin src="/assets/index-Cw6Vy2ED.js"></script>
|
<script type="module" crossorigin src="/assets/index-B-Yfb7Zz.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-HWj_Y8q4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-HWj_Y8q4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { lazy, Suspense } 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 { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import Layout from './components/Layout';
|
import Layout from './components/Layout';
|
||||||
|
|
||||||
@@ -24,24 +25,26 @@ const LoadingFallback = () => (
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<MotionConfig skipAnimations={true}>
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Router>
|
||||||
<Routes>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<Route path="/login" element={<Login />} />
|
<Routes>
|
||||||
<Route path="/" element={
|
<Route path="/login" element={<Login />} />
|
||||||
<ProtectedRoute>
|
<Route path="/" element={
|
||||||
<Layout />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<Layout />
|
||||||
}>
|
</ProtectedRoute>
|
||||||
<Route index element={<Navigate to="/pos" replace />} />
|
}>
|
||||||
<Route path="pos" element={<POS />} />
|
<Route index element={<Navigate to="/pos" replace />} />
|
||||||
<Route path="orders" element={<Orders />} />
|
<Route path="pos" element={<POS />} />
|
||||||
<Route path="categories" element={<Categories />} />
|
<Route path="orders" element={<Orders />} />
|
||||||
<Route path="products" element={<Products />} />
|
<Route path="categories" element={<Categories />} />
|
||||||
</Route>
|
<Route path="products" element={<Products />} />
|
||||||
</Routes>
|
</Route>
|
||||||
</Suspense>
|
</Routes>
|
||||||
</Router>
|
</Suspense>
|
||||||
|
</Router>
|
||||||
|
</MotionConfig>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { lazy, Suspense } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
|
import { MotionConfig } from 'framer-motion';
|
||||||
import Layout from './components/Layout.tsx';
|
import Layout from './components/Layout.tsx';
|
||||||
|
|
||||||
// Lazy load pages to decrease initial bundle size
|
// Lazy load pages to decrease initial bundle size
|
||||||
@@ -47,66 +48,68 @@ const LoadingFallback = () => (
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<MotionConfig skipAnimations={true}>
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Router>
|
||||||
<Routes>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<Route path="/login" element={<Login />} />
|
<Routes>
|
||||||
<Route path="/" element={
|
<Route path="/login" element={<Login />} />
|
||||||
<ProtectedRoute>
|
<Route path="/" element={
|
||||||
<Layout />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<Layout />
|
||||||
}>
|
</ProtectedRoute>
|
||||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
}>
|
||||||
<Route path="dashboard" element={<Dashboard />} />
|
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||||
|
<Route path="dashboard" element={<Dashboard />} />
|
||||||
{/* Sale */}
|
|
||||||
<Route path="sale/orders" element={<Orders />} />
|
{/* Sale */}
|
||||||
<Route path="sale/archived-bills" element={<ArchivedOrders />} />
|
<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 />} />
|
{/* Purchases */}
|
||||||
<Route path="purchases/orders" element={<Purchases />} />
|
<Route path="purchases/dashboard" element={<VendorDashboard />} />
|
||||||
<Route path="purchases/summary" element={<PurchaseSummary />} />
|
<Route path="purchases/orders" element={<Purchases />} />
|
||||||
<Route path="purchases/bills" element={<Bills />} />
|
<Route path="purchases/summary" element={<PurchaseSummary />} />
|
||||||
<Route path="purchases/vendor" element={<Vendors />} />
|
<Route path="purchases/bills" element={<Bills />} />
|
||||||
<Route path="purchases/analytics" element={<PurchaseAnalytics />} />
|
<Route path="purchases/vendor" element={<Vendors />} />
|
||||||
|
<Route path="purchases/analytics" element={<PurchaseAnalytics />} />
|
||||||
{/* Intent */}
|
|
||||||
<Route path="purchases/intent/orders-dashboard" element={<IntentDashboard title="ORDER DASHBOARD" />} />
|
{/* Intent */}
|
||||||
<Route path="purchases/intent/receives-dashboard" element={<IntentDashboard title="RECEIVABLE DASHBOARD" />} />
|
<Route path="purchases/intent/orders-dashboard" element={<IntentDashboard title="ORDER DASHBOARD" />} />
|
||||||
<Route path="purchases/intent/orders" element={<IntentList title="ORDERS" />} />
|
<Route path="purchases/intent/receives-dashboard" element={<IntentDashboard title="RECEIVABLE DASHBOARD" />} />
|
||||||
<Route path="purchases/intent/receives" element={<IntentList title="RECEIVES" />} />
|
<Route path="purchases/intent/orders" element={<IntentList title="ORDERS" />} />
|
||||||
|
<Route path="purchases/intent/receives" element={<IntentList title="RECEIVES" />} />
|
||||||
|
|
||||||
{/* Inventory */}
|
{/* Inventory */}
|
||||||
<Route path="inventory/new-arrivals" element={<NewArrivals />} />
|
<Route path="inventory/new-arrivals" element={<NewArrivals />} />
|
||||||
<Route path="inventory/base" element={<BaseMenu />} />
|
<Route path="inventory/base" element={<BaseMenu />} />
|
||||||
<Route path="inventory/products" element={<Products />} />
|
<Route path="inventory/products" element={<Products />} />
|
||||||
<Route path="inventory/online" element={<Navigate to="/inventory/products" replace />} />
|
<Route path="inventory/online" element={<Navigate to="/inventory/products" replace />} />
|
||||||
|
|
||||||
{/* Others */}
|
{/* Others */}
|
||||||
<Route path="reports" element={<Reports />} />
|
<Route path="reports" element={<Reports />} />
|
||||||
<Route path="feedback" element={<Feedback />} />
|
<Route path="feedback" element={<Feedback />} />
|
||||||
<Route path="ritz/overview" element={<Ritz />} />
|
<Route path="ritz/overview" element={<Ritz />} />
|
||||||
<Route path="ritz/circulation" element={<RitzCirculation />} />
|
<Route path="ritz/circulation" element={<RitzCirculation />} />
|
||||||
<Route path="ritz/wallets" element={<ManageWallets />} />
|
<Route path="ritz/wallets" element={<ManageWallets />} />
|
||||||
<Route path="ritz/coupons" element={<ManageCoupons />} />
|
<Route path="ritz/coupons" element={<ManageCoupons />} />
|
||||||
|
|
||||||
{/* Stores */}
|
{/* Stores */}
|
||||||
<Route path="stores/terminals" element={<Terminals />} />
|
<Route path="stores/terminals" element={<Terminals />} />
|
||||||
<Route path="stores/managers" element={<Managers />} />
|
<Route path="stores/managers" element={<Managers />} />
|
||||||
<Route path="stores/staffs" element={<Staff />} />
|
<Route path="stores/staffs" element={<Staff />} />
|
||||||
<Route path="stores/stalls" element={<Stalls />} />
|
<Route path="stores/stalls" element={<Stalls />} />
|
||||||
|
|
||||||
{/* Settings */}
|
{/* Settings */}
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="settings" element={<Settings />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Router>
|
</Router>
|
||||||
|
</MotionConfig>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
import { lazy, Suspense } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
|
import { MotionConfig } from 'framer-motion';
|
||||||
import { CartProvider } from './contexts/CartContext';
|
import { CartProvider } from './contexts/CartContext';
|
||||||
import { AuthProvider } from './contexts/AuthContext';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
import { FoodProvider } from './contexts/FoodContext';
|
import { FoodProvider } from './contexts/FoodContext';
|
||||||
@@ -38,81 +39,83 @@ function App() {
|
|||||||
<FoodProvider>
|
<FoodProvider>
|
||||||
<CartProvider>
|
<CartProvider>
|
||||||
<StockAlert />
|
<StockAlert />
|
||||||
<Router>
|
<MotionConfig skipAnimations={true}>
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Router>
|
||||||
<Routes>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<Route path="/login" element={<LoginScreen />} />
|
<Routes>
|
||||||
|
<Route path="/login" element={<LoginScreen />} />
|
||||||
<Route path="/" element={
|
|
||||||
<ProtectedRoute>
|
<Route path="/" element={
|
||||||
<HomeScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<HomeScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/orders" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/orders" element={
|
||||||
<MyOrdersScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<MyOrdersScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/category/:categoryId" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/category/:categoryId" element={
|
||||||
<CategoryScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<CategoryScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/item/:itemId" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/item/:itemId" element={
|
||||||
<ItemDetailScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<ItemDetailScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/stall/:id" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/stall/:id" element={
|
||||||
<StallDetailScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<StallDetailScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
<Route path="/cart" element={
|
|
||||||
<ProtectedRoute>
|
<Route path="/cart" element={
|
||||||
<CartScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<CartScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/checkout" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/checkout" element={
|
||||||
<CheckoutScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<CheckoutScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/success" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/success" element={
|
||||||
<SuccessScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<SuccessScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/profile" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/profile" element={
|
||||||
<ProfileScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<ProfileScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/change-pin" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/change-pin" element={
|
||||||
<ChangePinScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<ChangePinScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/wallet" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/wallet" element={
|
||||||
<WalletScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<WalletScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/topup" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/topup" element={
|
||||||
<TopUpScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<TopUpScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/preferences" element={
|
} />
|
||||||
<ProtectedRoute>
|
<Route path="/preferences" element={
|
||||||
<PreferencesScreen />
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<PreferencesScreen />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="*" element={<NotFoundScreen />} />
|
} />
|
||||||
</Routes>
|
<Route path="*" element={<NotFoundScreen />} />
|
||||||
</Suspense>
|
</Routes>
|
||||||
</Router>
|
</Suspense>
|
||||||
|
</Router>
|
||||||
|
</MotionConfig>
|
||||||
</CartProvider>
|
</CartProvider>
|
||||||
</FoodProvider>
|
</FoodProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
BIN
ordering_site/src/assets/RIT_Logo.webp
Normal file
BIN
ordering_site/src/assets/RIT_Logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
ordering_site/src/assets/Tillo.png
Normal file
BIN
ordering_site/src/assets/Tillo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
ordering_site/src/assets/display_ritz.webp
Normal file
BIN
ordering_site/src/assets/display_ritz.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
BIN
ordering_site/src/assets/front.webp
Normal file
BIN
ordering_site/src/assets/front.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
ordering_site/src/assets/tillo_spinner.gif
Normal file
BIN
ordering_site/src/assets/tillo_spinner.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
68
ordering_site/src/components/StockAlert.tsx
Normal file
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
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
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
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();
|
||||||
46
ordering_site/src/pages/NotFoundScreen.tsx
Normal file
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
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;
|
||||||
358
ordering_site/src/pages/TopUpScreen.css
Normal file
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
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
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);
|
||||||
|
}
|
||||||
221
ordering_site/src/pages/WalletScreen.tsx
Normal file
221
ordering_site/src/pages/WalletScreen.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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(`http://${window.location.hostname}:8080/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(`http://${window.location.hostname}:8080/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(`http://${window.location.hostname}:8080/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;
|
||||||
192
ordering_site/src/utils/razorpay.ts
Normal file
192
ordering_site/src/utils/razorpay.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_HOST = () => `http://${window.location.hostname}:8080`;
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user