Code optimization & other

Memoization & other
This commit is contained in:
RailTH
2024-05-21 00:17:45 +11:00
parent c3700ad25a
commit dc028a0af0
55 changed files with 393 additions and 296 deletions

View File

@@ -1,7 +1,19 @@
{ {
"files": { "files": {
"main.css": "/static/css/main.a416014f.css", "main.css": "/static/css/main.ae5adad7.css",
"main.js": "/static/js/main.62117073.js", "main.js": "/static/js/main.782e588e.js",
"static/css/944.441ba2b0.chunk.css": "/static/css/944.441ba2b0.chunk.css",
"static/js/944.76541c4f.chunk.js": "/static/js/944.76541c4f.chunk.js",
"static/css/231.699eab71.chunk.css": "/static/css/231.699eab71.chunk.css",
"static/js/231.51bcdbf3.chunk.js": "/static/js/231.51bcdbf3.chunk.js",
"static/css/84.eee7bbf7.chunk.css": "/static/css/84.eee7bbf7.chunk.css",
"static/js/84.74f6e8ef.chunk.js": "/static/js/84.74f6e8ef.chunk.js",
"static/css/44.55ed669a.chunk.css": "/static/css/44.55ed669a.chunk.css",
"static/js/44.2ae32818.chunk.js": "/static/js/44.2ae32818.chunk.js",
"static/css/503.9bd9b336.chunk.css": "/static/css/503.9bd9b336.chunk.css",
"static/js/503.164b696e.chunk.js": "/static/js/503.164b696e.chunk.js",
"static/css/979.179629c8.chunk.css": "/static/css/979.179629c8.chunk.css",
"static/js/979.fd51a066.chunk.js": "/static/js/979.fd51a066.chunk.js",
"static/media/scam-image.png": "/static/media/scam-image.c6c14289dc251ba2d2b1.png", "static/media/scam-image.png": "/static/media/scam-image.c6c14289dc251ba2d2b1.png",
"static/media/info-page__railth-avatar.png": "/static/media/info-page__railth-avatar.cbf11c43b5ef243b38c0.png", "static/media/info-page__railth-avatar.png": "/static/media/info-page__railth-avatar.cbf11c43b5ef243b38c0.png",
"static/media/add.webp": "/static/media/add.cd69f1e2a8c91109db0f.webp", "static/media/add.webp": "/static/media/add.cd69f1e2a8c91109db0f.webp",
@@ -14,11 +26,23 @@
"static/media/rating__star-icon.svg": "/static/media/rating__star-icon.73718a24d04eb67f5873.svg", "static/media/rating__star-icon.svg": "/static/media/rating__star-icon.73718a24d04eb67f5873.svg",
"static/media/rating__filled-star-icon.svg": "/static/media/rating__filled-star-icon.dc7d908d4d943b7f3b56.svg", "static/media/rating__filled-star-icon.svg": "/static/media/rating__filled-star-icon.dc7d908d4d943b7f3b56.svg",
"index.html": "/index.html", "index.html": "/index.html",
"main.a416014f.css.map": "/static/css/main.a416014f.css.map", "main.ae5adad7.css.map": "/static/css/main.ae5adad7.css.map",
"main.62117073.js.map": "/static/js/main.62117073.js.map" "main.782e588e.js.map": "/static/js/main.782e588e.js.map",
"944.441ba2b0.chunk.css.map": "/static/css/944.441ba2b0.chunk.css.map",
"944.76541c4f.chunk.js.map": "/static/js/944.76541c4f.chunk.js.map",
"231.699eab71.chunk.css.map": "/static/css/231.699eab71.chunk.css.map",
"231.51bcdbf3.chunk.js.map": "/static/js/231.51bcdbf3.chunk.js.map",
"84.eee7bbf7.chunk.css.map": "/static/css/84.eee7bbf7.chunk.css.map",
"84.74f6e8ef.chunk.js.map": "/static/js/84.74f6e8ef.chunk.js.map",
"44.55ed669a.chunk.css.map": "/static/css/44.55ed669a.chunk.css.map",
"44.2ae32818.chunk.js.map": "/static/js/44.2ae32818.chunk.js.map",
"503.9bd9b336.chunk.css.map": "/static/css/503.9bd9b336.chunk.css.map",
"503.164b696e.chunk.js.map": "/static/js/503.164b696e.chunk.js.map",
"979.179629c8.chunk.css.map": "/static/css/979.179629c8.chunk.css.map",
"979.fd51a066.chunk.js.map": "/static/js/979.fd51a066.chunk.js.map"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.a416014f.css", "static/css/main.ae5adad7.css",
"static/js/main.62117073.js" "static/js/main.782e588e.js"
] ]
} }

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>SusMarket</title><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.62117073.js"></script><link href="/static/css/main.a416014f.css" rel="stylesheet"></head><body><div id="root"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>SusMarket</title><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.782e588e.js"></script><link href="/static/css/main.ae5adad7.css" rel="stylesheet"></head><body><div id="root"></div></body></html>

View File

@@ -0,0 +1,2 @@
.payment-page{align-items:center;display:flex;flex-direction:column;gap:20px;min-height:100%;width:96%}.payment-page .payment-page__price{color:#eb5e28;font-size:96px;font-weight:600;letter-spacing:0;line-height:80px;text-align:left}.payment-page .payment-page__payment-card{background:#252422;border-radius:20px;box-shadow:-4px -4px 10px 0 #00000040,4px 4px 10px 0 #00000040;box-sizing:border-box;display:flex;flex-direction:column;gap:20px;padding:20px;width:500px}.payment-page .payment-page__payment-card .payment-card__heading,.payment-page .payment-page__payment-card .payment-card__input{color:#fff;font-size:24px;font-weight:600;letter-spacing:0;line-height:29px;text-align:left}.payment-page .payment-page__payment-card .payment-card__input{align-items:center;border:2px solid #ccc5b9;border-radius:12px;box-sizing:border-box;display:flex;height:60px;padding:5px 20px;width:100%}.payment-page .payment-page__payment-card .payment-card__inputs-group{display:flex;gap:20px;justify-content:space-between}.payment-page .payment-page__pay-link{align-items:center;background-color:#eb5e28;border-radius:15px;color:#fff;display:flex;font-size:20px;font-weight:500;height:50px;justify-content:center;letter-spacing:0;line-height:29px;width:500px}
/*# sourceMappingURL=231.699eab71.chunk.css.map*/

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/css/231.699eab71.chunk.css","mappings":"AAIA,cAMI,mBAHA,YAAa,CACb,qBAAsB,CACtB,QAAS,CAHT,eAAgB,CADhB,SAKmB,CANvB,mCASQ,aAXc,CAYd,cAAe,CACf,eAAgB,CAEhB,gBAAkB,CADlB,gBAAiB,CAEjB,eAAgB,CAdxB,0CA0BQ,mBAFA,kBAAmB,CACnB,8DAAuF,CAHvF,qBAAsB,CAHtB,YAAa,CACb,qBAAsB,CACtB,QAAS,CAET,YAAa,CALb,WAtBkB,CAI1B,gIA6BY,UAAY,CACZ,cAAe,CACf,eAAgB,CAEhB,gBAAkB,CADlB,gBAAiB,CAEjB,eAiBgB,CAnD5B,+DAyCY,kBAAmB,CAGnB,wBA/CQ,CAgDR,kBAAmB,CAFnB,qBAAsB,CAHtB,YAAa,CADb,WAAY,CAGZ,gBAA0B,CAJ1B,UAagB,CAnD5B,sEAuDY,YAAa,CAEb,SADA,6BACS,CAzDrB,sCAkEQ,kBAAmB,CACnB,wBArEc,CAsEd,kBAAmB,CACnB,UAAY,CALZ,YAAa,CAMb,cAAe,CACf,eAAgB,CARhB,WAAY,CAEZ,sBAAuB,CAQvB,iBADA,gBAAiB,CAVjB,WAWkB","sources":["PaymentStyle.scss"],"sourcesContent":["$background-color: #252422;\r\n$main-color: #CCC5B9;\r\n$accent-color: #EB5E28;\r\n\r\n.payment-page {\r\n width: 96%;\r\n min-height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 20px;\r\n align-items: center;\r\n\r\n .payment-page__price {\r\n color: $accent-color;\r\n font-size: 96px;\r\n font-weight: 600;\r\n line-height: 80px;\r\n letter-spacing: 0%;\r\n text-align: left;\r\n }\r\n\r\n .payment-page__payment-card {\r\n width: 500px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 20px;\r\n box-sizing: border-box;\r\n padding: 20px;\r\n border-radius: 20px;\r\n box-shadow: -4px -4px 10px 0px rgba(0, 0, 0, 0.25),4px 4px 10px 0px rgba(0, 0, 0, 0.25);\r\n background: $background-color;\r\n\r\n .payment-card__heading {\r\n color: white;\r\n font-size: 24px;\r\n font-weight: 600;\r\n line-height: 29px;\r\n letter-spacing: 0%;\r\n text-align: left;\r\n }\r\n\r\n .payment-card__input {\r\n width: 100%;\r\n height: 60px;\r\n display: flex;\r\n align-items: center;\r\n padding: 5px 20px 5px 20px;\r\n box-sizing: border-box;\r\n border: 2px solid $main-color;\r\n border-radius: 12px;\r\n color: white;\r\n font-size: 24px;\r\n font-weight: 600;\r\n line-height: 29px;\r\n letter-spacing: 0%;\r\n text-align: left;\r\n }\r\n \r\n .payment-card__inputs-group {\r\n display: flex;\r\n justify-content: space-between;\r\n gap: 20px;\r\n }\r\n }\r\n\r\n .payment-page__pay-link {\r\n width: 500px;\r\n height: 50px;\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n background-color: $accent-color;\r\n border-radius: 15px;\r\n color: white;\r\n font-size: 20px;\r\n font-weight: 500;\r\n line-height: 29px;\r\n letter-spacing: 0%;\r\n }\r\n}"],"names":[],"sourceRoot":""}

View File

@@ -0,0 +1,2 @@
.profile-page{align-items:flex-start;display:flex;flex-direction:column;gap:20px;min-height:100%;width:96%}.profile-page .profile-page__nav{display:flex;justify-content:space-between;width:100%}.profile-page .profile-page__nav .profile-link{color:#fff;font-size:32px;font-weight:600;letter-spacing:0;line-height:39px}.profile-page .profile-page__nav .active{color:#eb5e28}.profile-page .profile-page__info-div{display:flex;flex-direction:column;max-width:150px}.profile-page .profile-page__info-div span{color:#fff;font-size:24px;font-weight:600;letter-spacing:0;line-height:29px}.profile-page .orders-section{display:flex;flex-direction:column;gap:20px;width:100%}.profile-page .orders-section .orders-container{display:flex;gap:80px;width:100%}.profile-page .orders-section .orders-container .orders-div{align-items:flex-start;display:flex;flex-direction:row;gap:40px;justify-content:flex-start}.profile-page .orders-section .orders-container .orders-div .order-article{align-items:center;background:#252422;border-radius:15px;box-shadow:-4px -4px 10px 0 #00000040,4px 4px 10px 0 #00000040;display:flex;flex-direction:row;height:120px;justify-content:space-between;padding:0 14px;width:352px}.profile-page .orders-section .orders-container .orders-div .order-article .order-article__img{background-position:50%;background-repeat:no-repeat;background-size:cover;border-radius:8px;height:90px;min-width:90px}.profile-page .orders-section .orders-container .orders-div .order-article .order-article__info-div{align-items:flex-start;display:flex;flex-direction:column;justify-content:center}.profile-page .orders-section .orders-container .orders-div .order-article .order-article__info-div .order-article__status-span{color:#fff;font-size:24px;font-weight:600;letter-spacing:0;line-height:29px}.profile-page .orders-section .orders-container .orders-div .order-article .order-article__info-div .order-article__info-span{color:grey;font-size:16px;font-weight:500;letter-spacing:0;line-height:20px}.profile-page .orders-section .orders-container .orders-div .order-article .order-article__info-div .order-article__date-span{color:#fff;font-size:16px;font-weight:500;letter-spacing:0;line-height:20px}.profile-page .purchases-section{display:flex;flex-direction:column;gap:20px;width:100%}.profile-page .purchases-section .purchases-container{display:flex;gap:80px;width:100%}.profile-page .purchases-section .purchases-container .purchases-div{display:flex}.profile-page .profile-page__logout-button{width:48px}
/*# sourceMappingURL=44.55ed669a.chunk.css.map*/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
.scam-page{display:flex;width:96%}.scam-page .scam-page__image{width:100%}
/*# sourceMappingURL=503.9bd9b336.chunk.css.map*/

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/css/503.9bd9b336.chunk.css","mappings":"AAAA,WAEI,aADA,SACa,CAFjB,6BAKQ,UAAW","sources":["ScamStyle.scss"],"sourcesContent":[".scam-page {\r\n width: 96%;\r\n display: flex;\r\n\r\n .scam-page__image {\r\n width: 100%;\r\n }\r\n}"],"names":[],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
.home-page{align-items:center;display:flex;flex-direction:column;min-height:100%;width:96%}.home-page .products-div{display:grid;flex-flow:wrap;grid-template-columns:repeat(auto-fill,260px);justify-content:space-between;padding-top:36px;width:100%}
/*# sourceMappingURL=944.441ba2b0.chunk.css.map*/

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/css/944.441ba2b0.chunk.css","mappings":"AAIA,WAKI,mBAFA,YAAa,CACb,qBAAsB,CAFtB,eAAgB,CADhB,SAImB,CALvB,yBASQ,YAAa,CAGb,cAAe,CAFf,6CAA+C,CAC/C,6BAA8B,CAE9B,iBALA,UAKiB","sources":["HomeStyle.scss"],"sourcesContent":["$background-color: #252422;\r\n$main-color: #CCC5B9;\r\n$accent-color: #EB5E28;\r\n\r\n.home-page {\r\n width: 96%;\r\n min-height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n\r\n .products-div {\r\n width: 100%;\r\n display: grid;\r\n grid-template-columns: repeat(auto-fill, 260px);\r\n justify-content: space-between;\r\n flex-flow: wrap;\r\n padding-top: 36px;\r\n }\r\n}"],"names":[],"sourceRoot":""}

View File

@@ -0,0 +1,2 @@
.info-page{align-items:center;display:flex;justify-content:space-around;min-height:100%;width:96%}.info-page .info-page__dev-card{border-radius:15px;height:400px;perspective:1000px;width:300px;z-index:1}.info-page .info-page__dev-card .dev-card__inner{border-radius:15px;box-shadow:-4px -4px 10px 0 #00000040,4px 4px 20px 0 #00000040;height:100%;position:relative;text-align:center;transform-style:preserve-3d;transition:transform .6s;width:100%;z-index:1}.info-page .info-page__dev-card .dev-card__inner .dev-card__back,.info-page .info-page__dev-card .dev-card__inner .dev-card__front{-webkit-backface-visibility:hidden;backface-visibility:hidden;border-radius:15px;height:100%;position:absolute;width:100%;z-index:1}.info-page .info-page__dev-card .dev-card__inner .dev-card__front .dev-card__avatar{border-radius:15px;height:100%;width:100%}.info-page .info-page__dev-card .dev-card__inner .dev-card__back{background-color:#252422;box-sizing:border-box;color:#fff;display:flex;flex-direction:column;justify-content:space-between;padding:20px;transform:rotateY(180deg)}.info-page .info-page__dev-card .dev-card__inner .dev-card__back .dev-card__name{font-size:24px}.info-page .info-page__dev-card .dev-card__inner .dev-card__back .dev-card__info{color:#ccc5b9}.info-page .info-page__dev-card .dev-card__inner .dev-card__back .dev-card__url{color:#fff;font-size:18px;text-decoration:underline}.info-page .info-page__dev-card:hover .dev-card__inner{transform:rotateY(180deg)}
/*# sourceMappingURL=979.179629c8.chunk.css.map*/

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/css/979.179629c8.chunk.css","mappings":"AAIA,WAII,kBAAmB,CADnB,YAAa,CAEb,6BAHA,eAAgB,CADhB,SAI6B,CALjC,gCAWQ,kBAAmB,CAFnB,YAAa,CACb,kBAAmB,CAFnB,WAAY,CAIZ,SAAU,CAZlB,iDAiBY,kBAAmB,CAMnB,+DAJA,WAAY,CAJZ,iBAAkB,CAKlB,iBAAkB,CAElB,2BAA4B,CAD5B,wBAA0B,CAH1B,UAAW,CAFX,SAOuF,CAvBnG,mIA+BgB,8DAHA,kBAAmB,CAEnB,WAAY,CAJZ,iBAAkB,CAGlB,UAAW,CAFX,SAI2B,CA/B3C,oFAsCoB,mBADA,WAAY,CADZ,UAEmB,CAtCvC,iEA2CgB,wBA/CU,CAiDV,qBAAsB,CADtB,UAAY,CAIZ,YAAa,CACb,qBAAsB,CACtB,8BAJA,YAAa,CACb,yBAG8B,CAlD9C,iFAqDoB,cAAe,CArDnC,iFAyDoB,aA5DA,CAGpB,gFA8DoB,UAAY,CADZ,cAAe,CAEf,yBAA0B,CA/D9C,uDAsEQ,yBAA0B","sources":["InfoPageStyle.scss"],"sourcesContent":["$background-color: #252422;\r\n$main-color: #CCC5B9;\r\n$accent-color: #EB5E28;\r\n\r\n.info-page {\r\n width: 96%;\r\n min-height: 100%;\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-around;\r\n\r\n .info-page__dev-card {\r\n width: 300px;\r\n height: 400px;\r\n perspective: 1000px;\r\n border-radius: 15px;\r\n z-index: 1;\r\n \r\n .dev-card__inner {\r\n position: relative;\r\n z-index: 1;\r\n border-radius: 15px;\r\n width: 100%;\r\n height: 100%;\r\n text-align: center;\r\n transition: transform 0.6s;\r\n transform-style: preserve-3d;\r\n box-shadow: -4px -4px 10px 0px rgba(0, 0, 0, 0.25),4px 4px 20px 0px rgba(0, 0, 0, 0.25);\r\n \r\n .dev-card__front, .dev-card__back {\r\n position: absolute;\r\n z-index: 1;\r\n border-radius: 15px;\r\n width: 100%;\r\n height: 100%;\r\n backface-visibility: hidden;\r\n }\r\n \r\n .dev-card__front {\r\n .dev-card__avatar {\r\n width: 100%;\r\n height: 100%;\r\n border-radius: 15px;\r\n }\r\n }\r\n \r\n .dev-card__back {\r\n background-color: $background-color;\r\n color: white;\r\n box-sizing: border-box;\r\n padding: 20px;\r\n transform: rotateY(180deg);\r\n display: flex;\r\n flex-direction: column;\r\n justify-content: space-between;\r\n\r\n .dev-card__name {\r\n font-size: 24px;\r\n }\r\n\r\n .dev-card__info {\r\n color: $main-color;\r\n }\r\n\r\n .dev-card__url {\r\n font-size: 18px;\r\n color: white;\r\n text-decoration: underline;\r\n }\r\n }\r\n }\r\n }\r\n \r\n .info-page__dev-card:hover .dev-card__inner {\r\n transform: rotateY(180deg);\r\n }\r\n}"],"names":[],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
"use strict";(self.webpackChunkreactapp=self.webpackChunkreactapp||[]).push([[231],{231:(e,a,t)=>{t.r(a),t.d(a,{default:()=>l});var s=t(791),c=t(689),n=t(184);const l=function(){const[e,a]=(0,s.useState)(""),[t,l]=(0,s.useState)(""),[p,r]=(0,s.useState)(""),u=(0,c.TH)(),i=new URLSearchParams(u.search).get("price"),h=(0,s.useCallback)((e=>{var t;const s=(null===(t=e.target.value.replace(/\D/g,"").match(/.{1,4}/g))||void 0===t?void 0:t.join(" "))||"";a(s)}),[]),m=(0,s.useCallback)((e=>{const a=e.target.value.replace(/\D/g,"");l(a.slice(0,4))}),[]),d=(0,s.useCallback)((e=>{const a=e.target.value.replace(/\D/g,"");r(a.slice(0,3))}),[]);return(0,n.jsxs)("section",{className:"payment-page",children:[(0,n.jsxs)("h2",{className:"payment-page__price",children:[i," \u20bd"]}),(0,n.jsxs)("div",{className:"payment-page__payment-card",children:[(0,n.jsx)("h3",{className:"payment-card__heading",children:"\u041e\u043f\u043b\u0430\u0442\u0430 \u043a\u0430\u0440\u0442\u043e\u0439"}),(0,n.jsx)("input",{className:"payment-card__input",type:"text",placeholder:"\u041d\u043e\u043c\u0435\u0440",value:e,onChange:h,maxLength:19}),(0,n.jsxs)("div",{className:"payment-card__inputs-group",children:[(0,n.jsx)("input",{className:"payment-card__input",type:"text",placeholder:"\u041c\u041c\u0413\u0413",value:t,onChange:m,maxLength:4}),(0,n.jsx)("input",{className:"payment-card__input",type:"text",placeholder:"CVC/CVV",value:p,onChange:d,maxLength:3})]})]}),(0,n.jsx)("a",{href:"scam",className:"payment-page__pay-link",children:"\u041e\u043f\u043b\u0430\u0442\u0438\u0442\u044c"})]})}}}]);
//# sourceMappingURL=231.51bcdbf3.chunk.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
"use strict";(self.webpackChunkreactapp=self.webpackChunkreactapp||[]).push([[44],{671:(s,e,a)=>{a.d(e,{Z:()=>c});a(791);var r=a(184);const c=function(){return(0,r.jsx)("div",{className:"banner-div"})}},44:(s,e,a)=>{a.r(e),a.d(e,{default:()=>x});a(791);var r=a(671),c=a(793),i=a(689),n=a(87);const l=a.p+"static/media/profile-avatar.1823777d20902d836fddbbcbc324756f.svg";var t=a(329),d=a(184);const o=function(){const s=t.Z.get("user");return(0,d.jsxs)("div",{className:"profile-page__info-div",children:[(0,d.jsx)("img",{src:l,alt:"",className:"info-div__img"}),(0,d.jsx)("span",{children:s||"\u0413\u043e\u0441\u0442\u044c"})]})};const p=function(){return(0,d.jsxs)("section",{className:"orders-section",children:[(0,d.jsxs)("nav",{className:"profile-page__nav",children:[(0,d.jsx)(n.rU,{to:"/",className:"profile-link active",children:"\u041c\u043e\u0438 \u0437\u0430\u043a\u0430\u0437\u044b"}),(0,d.jsx)(n.rU,{to:"purchases",className:"profile-link",children:"\u041c\u043e\u0438 \u043f\u043e\u043a\u0443\u043f\u043a\u0438"})]}),(0,d.jsxs)("div",{className:"orders-container",children:[(0,d.jsx)(o,{}),(0,d.jsxs)("div",{className:"orders-div",children:[(0,d.jsxs)("article",{className:"order-article",children:[(0,d.jsx)("div",{className:"order-article__img"}),(0,d.jsxs)("div",{className:"order-article__info-div",children:[(0,d.jsx)("span",{className:"order-article__status-span",children:"\u0412 \u043f\u0443\u0442\u0438"}),(0,d.jsx)("span",{className:"order-article__info-span",children:"\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u0432 \u043f\u0443\u043d\u043a\u0442 \u0432\u044b\u0434\u0430\u0447\u0438"}),(0,d.jsx)("span",{className:"order-article__date-span",children:"\u041e\u0436\u0438\u0434\u0430\u0435\u043c 9 \u0434\u0435\u043a\u0430\u0431\u0440\u044f"})]})]}),(0,d.jsxs)("article",{className:"order-article",children:[(0,d.jsx)("div",{className:"order-article__img"}),(0,d.jsxs)("div",{className:"order-article__info-div",children:[(0,d.jsx)("span",{className:"order-article__status-span",children:"\u0412 \u043f\u0443\u0442\u0438"}),(0,d.jsx)("span",{className:"order-article__info-span",children:"\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u0432 \u043f\u0443\u043d\u043a\u0442 \u0432\u044b\u0434\u0430\u0447\u0438"}),(0,d.jsx)("span",{className:"order-article__date-span",children:"\u041e\u0436\u0438\u0434\u0430\u0435\u043c 9 \u0434\u0435\u043a\u0430\u0431\u0440\u044f"})]})]})]})]})]})};const m=function(){return(0,d.jsxs)("section",{className:"purchases-section",children:[(0,d.jsxs)("nav",{className:"profile-page__nav",children:[(0,d.jsx)(n.rU,{to:"/profile",className:"profile-link",children:"\u041c\u043e\u0438 \u0437\u0430\u043a\u0430\u0437\u044b"}),(0,d.jsx)(n.rU,{to:"purchases",className:"profile-link active",children:"\u041c\u043e\u0438 \u043f\u043e\u043a\u0443\u043f\u043a\u0438"})]}),(0,d.jsxs)("div",{className:"purchases-container",children:[(0,d.jsx)(o,{}),(0,d.jsx)("div",{className:"purchases-div"})]})]})};const j=a.p+"static/media/logout-icon.edc99b580ff0f8975b05fdac4e38046c.svg";const x=function(){const s=(0,i.s0)();return(0,d.jsxs)("section",{className:"profile-page",children:[(0,d.jsx)(r.Z,{}),(0,d.jsxs)(i.Z5,{children:[(0,d.jsx)(i.AW,{path:"/",element:(0,d.jsx)(p,{})}),(0,d.jsx)(i.AW,{path:"purchases",element:(0,d.jsx)(m,{})})]}),(0,d.jsx)(c.E.button,{className:"profile-page__logout-button",whileTap:{scale:.9},transition:{duration:.2,type:"spring"},onClick:()=>{t.Z.remove("user"),t.Z.remove("user_id"),s("/")},children:(0,d.jsx)("img",{src:j,alt:"Logout icon"})})]})}}}]);
//# sourceMappingURL=44.2ae32818.chunk.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
"use strict";(self.webpackChunkreactapp=self.webpackChunkreactapp||[]).push([[503],{78:(a,c,s)=>{s.r(c),s.d(c,{default:()=>m});s(791);const e=s.p+"static/media/scam-image.c6c14289dc251ba2d2b1.png";var t=s(184);const m=function(){return(0,t.jsx)("section",{className:"scam-page",children:(0,t.jsx)("img",{src:e,alt:"scam mammoth",className:"scam-page__image"})})}}}]);
//# sourceMappingURL=503.164b696e.chunk.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/js/503.164b696e.chunk.js","mappings":"kNAYA,QARA,WACI,OACIA,EAAAA,EAAAA,KAAA,WAASC,UAAU,YAAWC,UAC1BF,EAAAA,EAAAA,KAAA,OAAKG,IAAKC,EAAWC,IAAI,eAAeJ,UAAU,sBAG9D,C","sources":["pages/ScamPage.tsx"],"sourcesContent":["import React from \"react\";\r\nimport '../ScamStyle.scss';\r\nimport ScamImage from \"../assets/img/scam-image.png\";\r\n\r\nfunction ScamPage() {\r\n return(\r\n <section className=\"scam-page\">\r\n <img src={ScamImage} alt=\"scam mammoth\" className=\"scam-page__image\"/>\r\n </section>\r\n )\r\n}\r\n\r\nexport default ScamPage;"],"names":["_jsx","className","children","src","ScamImage","alt"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
"use strict";(self.webpackChunkreactapp=self.webpackChunkreactapp||[]).push([[944],{671:(c,e,s)=>{s.d(e,{Z:()=>r});s(791);var i=s(184);const r=function(){return(0,i.jsx)("div",{className:"banner-div"})}},944:(c,e,s)=>{s.r(e),s.d(e,{default:()=>l});s(791);var i=s(87),r=s(793),a=s(184);const t=function(c){let{icons:e,title:s,price:i}=c;const t=i.toLocaleString("ru-RU");return(0,a.jsxs)(r.E.article,{className:"product-article",whileTap:{scale:.98},transition:{duration:.1,type:"spring"},whileHover:{boxShadow:"-4px -4px 10px 0px rgba(0, 0, 0, 0.25),4px 4px 20px 0px rgba(0, 0, 0, 0.25)"},children:[(0,a.jsx)("img",{src:e,alt:s,className:"product-article__img",loading:"lazy"}),(0,a.jsxs)("h5",{className:"product-article__price-h5",children:[(0,a.jsx)("span",{children:t}),(0,a.jsx)("span",{children:"\u20bd"})]}),(0,a.jsx)("h6",{className:"product-article__name-h6",children:s})]})};var n=s(671);const l=function(c){let{products:e}=c;return(0,a.jsxs)("section",{className:"home-page",children:[(0,a.jsx)(n.Z,{}),(0,a.jsx)("div",{className:"products-div",children:e.map((c=>(0,a.jsx)(i.rU,{to:"/product/".concat(c.id),children:(0,a.jsx)(t,{title:c.title,tags:c.tags,id:c.id,category_id:c.category_id,price:c.price,icons:c.icons,description:c.description})},c.id)))})]})}}}]);
//# sourceMappingURL=944.76541c4f.chunk.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/js/944.76541c4f.chunk.js","mappings":"uIASA,QAPA,WACI,OACIA,EAAAA,EAAAA,KAAA,OAAKC,UAAU,cAGvB,C,mFCmBA,QAtBA,SAAoBC,GAAoC,IAAnC,MAAEC,EAAK,MAAEC,EAAK,MAAEC,GAAgBH,EACjD,MAAMI,EAAgBD,EAAME,eAAe,SAE3C,OACIC,EAAAA,EAAAA,MAACC,EAAAA,EAAOC,QAAO,CACXT,UAAU,kBACVU,SAAU,CAACC,MAAO,KAClBC,WAAY,CAACC,SAAU,GAAKC,KAAM,UAClCC,WAAY,CAACC,UAAW,+EAA+EC,SAAA,EAEvGlB,EAAAA,EAAAA,KAAA,OAAKmB,IAAKhB,EAAOiB,IAAKhB,EAAOH,UAAU,uBAAuBoB,QAAQ,UACtEb,EAAAA,EAAAA,MAAA,MAAIP,UAAU,4BAA2BiB,SAAA,EACrClB,EAAAA,EAAAA,KAAA,QAAAkB,SAAOZ,KACPN,EAAAA,EAAAA,KAAA,QAAAkB,SAAM,eAEVlB,EAAAA,EAAAA,KAAA,MAAIC,UAAU,2BAA0BiB,SACnCd,MAIjB,E,aCUA,QAvBA,SAAiBF,GAA+B,IAA9B,SAAEoB,GAAyBpB,EACzC,OACIM,EAAAA,EAAAA,MAAA,WAASP,UAAU,YAAWiB,SAAA,EAC1BlB,EAAAA,EAAAA,KAACuB,EAAAA,EAAM,KACPvB,EAAAA,EAAAA,KAAA,OAAKC,UAAU,eAAciB,SACxBI,EAASE,KAAKC,IACXzB,EAAAA,EAAAA,KAAC0B,EAAAA,GAAI,CAACC,GAAE,YAAAC,OAAcH,EAAQI,IAAKX,UAC/BlB,EAAAA,EAAAA,KAAC8B,EAAW,CACR1B,MAAOqB,EAAQrB,MACf2B,KAAMN,EAAQM,KACdF,GAAIJ,EAAQI,GACZG,YAAaP,EAAQO,YACrB3B,MAAOoB,EAAQpB,MACfF,MAAOsB,EAAQtB,MACf8B,YAAaR,EAAQQ,eARYR,EAAQI,UAerE,C","sources":["components/AdBanner.tsx","components/ProductCard.tsx","pages/HomePage.tsx"],"sourcesContent":["import React from \"react\";\r\n\r\nfunction Banner() {\r\n return(\r\n <div className=\"banner-div\">\r\n </div>\r\n )\r\n}\r\n\r\nexport default Banner;","import React from \"react\";\r\nimport { motion } from \"framer-motion\";\r\nimport { Product } from \"../utils/types\";\r\n\r\nfunction ProductCard({ icons, title, price }: Product) {\r\n const priceAsString = price.toLocaleString('ru-RU');\r\n \r\n return(\r\n <motion.article\r\n className=\"product-article\"\r\n whileTap={{scale: 0.98}}\r\n transition={{duration: 0.1, type: \"spring\"}}\r\n whileHover={{boxShadow: \"-4px -4px 10px 0px rgba(0, 0, 0, 0.25),4px 4px 20px 0px rgba(0, 0, 0, 0.25)\"}}\r\n >\r\n <img src={icons} alt={title} className=\"product-article__img\" loading=\"lazy\"/>\r\n <h5 className=\"product-article__price-h5\">\r\n <span>{priceAsString}</span>\r\n <span>₽</span>\r\n </h5>\r\n <h6 className=\"product-article__name-h6\">\r\n {title}\r\n </h6>\r\n </motion.article>\r\n )\r\n}\r\n\r\nexport default ProductCard;","import React from \"react\";\r\nimport { Link } from \"react-router-dom\";\r\nimport '../HomeStyle.scss';\r\nimport ProductCard from \"../components/ProductCard\";\r\nimport Banner from \"../components/AdBanner\";\r\nimport { Product } from \"../utils/types\";\r\n\r\ntype HomePageProps = {\r\n products: Product[];\r\n}\r\n\r\nfunction HomePage({ products }: HomePageProps) {\r\n return(\r\n <section className=\"home-page\">\r\n <Banner />\r\n <div className=\"products-div\">\r\n {products.map((product) => (\r\n <Link to={`/product/${product.id}`} key={product.id}>\r\n <ProductCard\r\n title={product.title}\r\n tags={product.tags}\r\n id={product.id}\r\n category_id={product.category_id}\r\n price={product.price}\r\n icons={product.icons}\r\n description={product.description}\r\n />\r\n </Link>\r\n ))}\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default HomePage;"],"names":["_jsx","className","_ref","icons","title","price","priceAsString","toLocaleString","_jsxs","motion","article","whileTap","scale","transition","duration","type","whileHover","boxShadow","children","src","alt","loading","products","Banner","map","product","Link","to","concat","id","ProductCard","tags","category_id","description"],"sourceRoot":""}

View File

@@ -0,0 +1,2 @@
"use strict";(self.webpackChunkreactapp=self.webpackChunkreactapp||[]).push([[979],{940:(a,e,s)=>{s.r(e),s.d(e,{default:()=>d});s(791);var r=s(184);const n=function(a){let{avatar:e,name:s,info:n,url:c}=a;return(0,r.jsx)("div",{className:"info-page__dev-card",children:(0,r.jsxs)("div",{className:"dev-card__inner",children:[(0,r.jsx)("div",{className:"dev-card__front",children:(0,r.jsx)("img",{src:e,alt:s,className:"dev-card__avatar"})}),(0,r.jsxs)("div",{className:"dev-card__back",children:[(0,r.jsxs)("div",{children:[(0,r.jsx)("h3",{className:"dev-card__name",children:s}),(0,r.jsx)("p",{className:"dev-card__info",children:n})]}),(0,r.jsx)("a",{className:"dev-card__url",href:c,target:"_blank",rel:"noreferrer",children:"GitHub"})]})]})})},c=s.p+"static/media/info-page__railth-avatar.cbf11c43b5ef243b38c0.png",i=s.p+"static/media/info-page__no-kesspen-avatar.baa74b50e31a8363436b.png";const d=function(){return(0,r.jsxs)("section",{className:"info-page",children:[(0,r.jsx)(n,{avatar:i,name:"No_Kesspen",info:"Backend & Frontend \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a",url:"https://github.com/KessPenGames"}),(0,r.jsx)(n,{avatar:c,name:"Rail_TH",info:"Frontend \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a",url:"https://github.com/Rail-TH"})]})}}}]);
//# sourceMappingURL=979.fd51a066.chunk.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/js/979.fd51a066.chunk.js","mappings":"oJAsBA,QAnBA,SAAgBA,GAA8C,IAA7C,OAAEC,EAAM,KAAEC,EAAI,KAAEC,EAAI,IAAEC,GAAoBJ,EACzD,OACEK,EAAAA,EAAAA,KAAA,OAAKC,UAAU,sBAAqBC,UAClCC,EAAAA,EAAAA,MAAA,OAAKF,UAAU,kBAAiBC,SAAA,EAC9BF,EAAAA,EAAAA,KAAA,OAAKC,UAAU,kBAAiBC,UAC9BF,EAAAA,EAAAA,KAAA,OAAKI,IAAKR,EAAQS,IAAKR,EAAMI,UAAU,wBAEzCE,EAAAA,EAAAA,MAAA,OAAKF,UAAU,iBAAgBC,SAAA,EAC7BC,EAAAA,EAAAA,MAAA,OAAAD,SAAA,EACEF,EAAAA,EAAAA,KAAA,MAAIC,UAAU,iBAAgBC,SAAEL,KAChCG,EAAAA,EAAAA,KAAA,KAAGC,UAAU,iBAAgBC,SAAEJ,QAEjCE,EAAAA,EAAAA,KAAA,KAAGC,UAAU,gBAAgBK,KAAMP,EAAKQ,OAAO,SAASC,IAAI,aAAYN,SAAC,kBAKnF,E,kJCKA,QAnBA,WACI,OACIC,EAAAA,EAAAA,MAAA,WAASF,UAAU,YAAWC,SAAA,EAC1BF,EAAAA,EAAAA,KAACS,EAAO,CACJb,OAAQc,EACRb,KAAK,aACLC,KAAK,wFACLC,IAAI,qCAERC,EAAAA,EAAAA,KAACS,EAAO,CACJb,OAAQe,EACRd,KAAK,UACLC,KAAK,8EACLC,IAAI,iCAIpB,C","sources":["components/DevCard.tsx","pages/InfoPage.tsx"],"sourcesContent":["import React from 'react';\r\nimport { DeveloperCard } from \"../utils/types\"\r\n\r\nfunction DevCard({ avatar, name, info, url }: DeveloperCard) {\r\n return (\r\n <div className=\"info-page__dev-card\">\r\n <div className=\"dev-card__inner\">\r\n <div className=\"dev-card__front\">\r\n <img src={avatar} alt={name} className=\"dev-card__avatar\" />\r\n </div>\r\n <div className=\"dev-card__back\">\r\n <div>\r\n <h3 className='dev-card__name'>{name}</h3>\r\n <p className='dev-card__info'>{info}</p>\r\n </div>\r\n <a className='dev-card__url' href={url} target='_blank' rel=\"noreferrer\">GitHub</a>\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\nexport default DevCard;","import React from \"react\";\r\nimport DevCard from \"../components/DevCard\";\r\nimport \"../InfoPageStyle.scss\";\r\nimport RailTHAvatar from \"../assets/img/info-page__railth-avatar.png\";\r\nimport NoKesspenAvatar from \"../assets/img/info-page__no-kesspen-avatar.png\";\r\n\r\nfunction InfoPage() {\r\n return (\r\n <section className=\"info-page\">\r\n <DevCard \r\n avatar={NoKesspenAvatar}\r\n name=\"No_Kesspen\"\r\n info=\"Backend & Frontend разработчик\"\r\n url=\"https://github.com/KessPenGames\"\r\n />\r\n <DevCard \r\n avatar={RailTHAvatar}\r\n name=\"Rail_TH\"\r\n info=\"Frontend разработчик\"\r\n url=\"https://github.com/Rail-TH\"\r\n />\r\n </section>\r\n );\r\n}\r\n\r\nexport default InfoPage;"],"names":["_ref","avatar","name","info","url","_jsx","className","children","_jsxs","src","alt","href","target","rel","DevCard","NoKesspenAvatar","RailTHAvatar"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,59 +1,55 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import axios from 'axios'; import axios from 'axios';
import HomePage from "./pages/HomePage";
import PaymentPage from "./pages/PaymentPage";
import ProductPage from "./pages/ProductPage";
import ProfilePage from "./pages/ProfilePage";
import ScamPage from "./pages/ScamPage";
import InfoPage from "./pages/InfoPage";
import Header from "./components/Header"; import Header from "./components/Header";
import PopupMap from "./components/PopupMap"; import PopupMap from "./components/PopupMap";
import { Product, Category } from "./utils/types"; import { Product, Category } from "./utils/types";
interface AppPopupMapState { // Lazy load pages for better performance
isPopupMapVisible: boolean; const LazyHomePage = React.lazy(() => import("./pages/HomePage"));
} const LazyPaymentPage = React.lazy(() => import("./pages/PaymentPage"));
const LazyProductPage = React.lazy(() => import("./pages/ProductPage"));
const LazyProfilePage = React.lazy(() => import("./pages/ProfilePage"));
const LazyScamPage = React.lazy(() => import("./pages/ScamPage"));
const LazyInfoPage = React.lazy(() => import("./pages/InfoPage"));
export default function App() { export default function App() {
const [state, setState] = useState<AppPopupMapState>({ isPopupMapVisible: false }); const [isPopupMapVisible, setIsPopupMapVisible] = useState(false);
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
const [selectedCategory, setSelectedCategory] = useState<Category | 'all'>('all'); const [selectedCategory, setSelectedCategory] = useState<Category | 'all'>('all');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
useEffect(() => { useEffect(() => {
axios.get('http://127.0.0.1:8000/api/get/products') const fetchProducts = async () => {
.then(response => { try {
const response = await axios.get('http://127.0.0.1:8000/api/get/products');
setProducts(response.data.products); setProducts(response.data.products);
}) } catch (error) {
.catch(error => {
console.error('Error fetching the products:', error); console.error('Error fetching the products:', error);
}); }
};
fetchProducts();
}, []); }, []);
const togglePopupMap = () => { const togglePopupMap = useCallback(() => {
setState(prevState => { setIsPopupMapVisible(prevState => {
if (!prevState.isPopupMapVisible) { document.body.classList.toggle('no-scroll', !prevState);
document.body.classList.add('no-scroll'); return !prevState;
} else {
document.body.classList.remove('no-scroll');
}
return { ...prevState, isPopupMapVisible: !prevState.isPopupMapVisible };
}); });
}; }, []);
const handleSearchChange = (query: string) => { const handleSearchChange = useCallback((query: string) => {
setSearchQuery(query); setSearchQuery(query);
}; }, []);
const filteredProducts = products.filter(product => const filteredProducts = useMemo(() => products.filter(product =>
(selectedCategory === 'all' || product.category_id === selectedCategory.id) && (selectedCategory === 'all' || product.category_id === selectedCategory.id) &&
product.title.toLowerCase().includes(searchQuery.toLowerCase()) product.title.toLowerCase().includes(searchQuery.toLowerCase())
); ), [products, selectedCategory, searchQuery]);
const handleSelectCategory = (category: Category | 'all') => { const handleSelectCategory = useCallback((category: Category | 'all') => {
setSelectedCategory(category); setSelectedCategory(category);
}; }, []);
return ( return (
<> <>
@@ -62,16 +58,18 @@ export default function App() {
onSelectCategory={handleSelectCategory} onSelectCategory={handleSelectCategory}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
/> />
{state.isPopupMapVisible && <PopupMap togglePopupMap={togglePopupMap} />} {isPopupMapVisible && <PopupMap togglePopupMap={togglePopupMap} />}
<main className="main"> <main className="main">
<Routes> <React.Suspense fallback={<div>Loading...</div>}>
<Route path="/" element={<HomePage products={filteredProducts} />} /> <Routes>
<Route path="profile/*" element={<ProfilePage />} /> <Route path="/" element={<LazyHomePage products={filteredProducts} />} />
<Route path="product/:id" element={<ProductPage />} /> <Route path="profile/*" element={<LazyProfilePage />} />
<Route path="payment" element={<PaymentPage />} /> <Route path="product/:id" element={<LazyProductPage />} />
<Route path="scam" element={<ScamPage />} /> <Route path="payment" element={<LazyPaymentPage />} />
<Route path="info" element={<InfoPage />} /> <Route path="scam" element={<LazyScamPage />} />
</Routes> <Route path="info" element={<LazyInfoPage />} />
</Routes>
</React.Suspense>
</main> </main>
</> </>
); );

View File

@@ -1,8 +1,10 @@
import React from "react"; import React from "react";
export default function Banner() { function Banner() {
return( return(
<div className="banner-div"> <div className="banner-div">
</div> </div>
) )
} }
export default Banner;

View File

@@ -7,7 +7,7 @@ interface CatalogMenuProps { // Пропсы, которые компонент
onSelectCategory: (category: Category | 'all') => void; // Функция для выбора категории onSelectCategory: (category: Category | 'all') => void; // Функция для выбора категории
} }
export default function CatalogMenu({ toggleCatalogMenu, onSelectCategory }: CatalogMenuProps) { function CatalogMenu({ toggleCatalogMenu, onSelectCategory }: CatalogMenuProps) {
const [categories, setCategories] = useState<Category[]>([]); // Состояние для хранения категорий const [categories, setCategories] = useState<Category[]>([]); // Состояние для хранения категорий
useEffect(() => { // При монтировании компонента запрашиваем категории с сервера useEffect(() => { // При монтировании компонента запрашиваем категории с сервера
@@ -45,4 +45,6 @@ export default function CatalogMenu({ toggleCatalogMenu, onSelectCategory }: Cat
</ul> </ul>
</> </>
); );
} }
export default CatalogMenu;

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { DeveloperCard } from "../utils/types" import { DeveloperCard } from "../utils/types"
export default function DevCard({ avatar, name, info, url }: DeveloperCard) { function DevCard({ avatar, name, info, url }: DeveloperCard) {
return ( return (
<div className="info-page__dev-card"> <div className="info-page__dev-card">
<div className="dev-card__inner"> <div className="dev-card__inner">
@@ -18,4 +18,6 @@ export default function DevCard({ avatar, name, info, url }: DeveloperCard) {
</div> </div>
</div> </div>
); );
} }
export default DevCard;

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useCallback } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import Logotype from '../assets/img/amongasik.png'; import Logotype from '../assets/img/amongasik.png';
@@ -7,34 +7,34 @@ import LoginMenu from './LoginMenu';
import { Category } from '../utils/types'; import { Category } from '../utils/types';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
interface HeaderProps { // Интерфейс для пропсов компонента Header interface HeaderProps {
togglePopupMap: () => void; // Функция для переключения видимости карты togglePopupMap: () => void;
onSelectCategory: (category: Category | 'all') => void; // Функция для выбора категории onSelectCategory: (category: Category | 'all') => void;
onSearchChange: (query: string) => void; // Функция для изменения строки поиска onSearchChange: (query: string) => void;
} }
const MotionLink = motion(Link); // Вынесение компонента в отдельную переменную для удобства использования const MotionLink = motion(Link); // Вынесение компонента в отдельную переменную для удобства использования
export default function Header({ togglePopupMap, onSelectCategory, onSearchChange }: HeaderProps) { function Header({ togglePopupMap, onSelectCategory, onSearchChange }: HeaderProps) {
const [isCatalogMenuVisible, setIsCatalogMenuVisible] = useState(false); // Состояние для хранения видимости карточного меню const [isCatalogMenuVisible, setIsCatalogMenuVisible] = useState(false);
const [isLoginMenuVisible, setIsLoginMenuVisible] = useState(false); // Состояние для хранения видимости меню входа const [isLoginMenuVisible, setIsLoginMenuVisible] = useState(false);
const navigate = useNavigate(); // Функция для навигации const navigate = useNavigate();
const toggleCatalogMenu = () => setIsCatalogMenuVisible(prevState => !prevState); // Функция для переключения видимости карточного меню const toggleCatalogMenu = useCallback(() => setIsCatalogMenuVisible(prevState => !prevState), []);
const toggleLoginMenu = () => setIsLoginMenuVisible(prevState => !prevState); // Функция для переключения видимости меню входа const toggleLoginMenu = useCallback(() => setIsLoginMenuVisible(prevState => !prevState), []);
const handleProfileClick = () => { // Функция для перехода на страницу профиля при нажатии на кнопку const handleProfileClick = useCallback(() => {
const userCookie = Cookies.get('user'); // Проверка на наличие куки с логином const userCookie = Cookies.get('user');
userCookie ? navigate('/profile') : toggleLoginMenu(); // Переход на страницу профиля если куки есть, иначе переключение видимости меню входа userCookie ? navigate('/profile') : toggleLoginMenu();
}; }, [navigate, toggleLoginMenu]);
const resetCategoryFilter = () => onSelectCategory('all'); // Функция для сброса фильтрации категорий const resetCategoryFilter = useCallback(() => onSelectCategory('all'), [onSelectCategory]);
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { // Функция для обработки нажатия клавиши Enter в поле ввода const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') { // Предотвращение отправки формы при нажатии Enter if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
} }
}; }, []);
return( return(
<header className="header"> <header className="header">
@@ -147,4 +147,6 @@ export default function Header({ togglePopupMap, onSelectCategory, onSearchChang
{isLoginMenuVisible && <LoginMenu toggleLoginMenu={toggleLoginMenu}/>} {isLoginMenuVisible && <LoginMenu toggleLoginMenu={toggleLoginMenu}/>}
</header> </header>
) )
} }
export default Header;

View File

@@ -1,64 +1,62 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import axios from 'axios'; import axios from 'axios';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
interface LoginMenuProps { // Интерфейс для пропсов компонента LoginMenu interface LoginMenuProps {
toggleLoginMenu: () => void; // Функция для переключения видимости меню входа toggleLoginMenu: () => void;
} }
export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps) { function LoginMenu({ toggleLoginMenu }: LoginMenuProps) {
const [isLoginMode, setIsLoginMode] = useState(true); // Состояние для определения режима входа или регистрации const [isLoginMode, setIsLoginMode] = useState(true);
const [login, setLogin] = useState(''); // Состояние для хранения введенного логина const [login, setLogin] = useState('');
const [password, setPassword] = useState(''); // Состояние для хранения введенного пароля const [password, setPassword] = useState('');
const navigate = useNavigate(); // Функция для навигации const navigate = useNavigate();
const toggleMode = () => setIsLoginMode(!isLoginMode); // Функция для переключения режима входа или регистрации const toggleMode = useCallback(() => setIsLoginMode(prev => !prev), []);
const handleClose = () => { // Функция для закрытия меню входа const handleClose = useCallback(() => {
document.body.classList.remove('no-scroll'); // Удаление класса "no-scroll" с тела документа document.body.classList.remove('no-scroll');
toggleLoginMenu(); // Вызов функции переключения видимости меню входа toggleLoginMenu();
}; }, [toggleLoginMenu]);
useEffect(() => { // Эффект для добавления класса "no-scroll" с тела документа при монтировании компонента useEffect(() => {
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
return () => { return () => {
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
}; };
}, []); }, []);
const handleAuth = async (isRegistering: boolean) => { // Функция для обработки авторизации const handleAuth = async (isRegistering: boolean) => {
const baseUrl = window.location.origin; // Получаем текущий домен сайта const baseUrl = window.location.origin;
try {
let response;
if (isRegistering) {
response = await axios.get(
`${baseUrl}/api/post/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`
);
} else {
response = await axios.get(
`${baseUrl}/api/get/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`
);
if (response.data.user.length === 0) {
alert('Пользователь не найден.');
return;
}
}
if (response.status === 200) { try {
Cookies.set('user', login, { expires: 1 }); // Установка куки с логином const endpoint = isRegistering
Cookies.set('user_id', response.data.user[0].id, { expires: 1 }); // Установка куки с ID пользователя ? `${baseUrl}/api/post/user`
navigate('/profile'); // Переход на страницу профиля : `${baseUrl}/api/get/user`;
toggleLoginMenu(); // Вызов функции переключения видимости меню входа
const params = new URLSearchParams({
login: encodeURIComponent(login),
password: encodeURIComponent(password),
});
const response = await axios.get(`${endpoint}?${params.toString()}`);
if (isRegistering || (response.data.user && response.data.user.length > 0)) {
Cookies.set('user', login, { expires: 1 });
Cookies.set('user_id', response.data.user[0].id, { expires: 1 });
navigate('/profile');
toggleLoginMenu();
} else {
alert('Пользователь не найден.');
} }
} catch (error) { } catch (error) {
alert('Ошибка при авторизации: ' + error); alert('Ошибка при авторизации: ' + error);
} }
}; };
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { // Функция для обработки отправки формы const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
await handleAuth(!isLoginMode); await handleAuth(!isLoginMode);
}; };
@@ -114,4 +112,6 @@ export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps) {
</form> </form>
</> </>
); );
} }
export default LoginMenu;

View File

@@ -1,26 +1,25 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
type ButtonState = 1 | 2 | null; type ButtonState = 1 | 2 | null;
interface PopupMapProps { // Пропсы, которые принимает компонент PopupMap interface PopupMapProps {
togglePopupMap: () => void; // Функция для закрытия всплывающего окна togglePopupMap: () => void;
} }
// Компонент, отображающий всплывающее окно с картой function PopupMap({ togglePopupMap }: PopupMapProps) {
export default function PopupMap({ togglePopupMap }: PopupMapProps) { const [selectedButton, setSelectedButton] = useState<ButtonState>(null);
const [selectedButton, setSelectedButton] = useState<ButtonState>(null); // Состояние для отслеживания выбранного кнопки
const handleButtonClick = (buttonId: ButtonState) => { // Обработчик клика на кнопку const handleButtonClick = useCallback((buttonId: ButtonState) => {
setSelectedButton(buttonId); setSelectedButton(buttonId);
}; }, []);
const handleClose = () => { // Обработчик закрытия всплывающего окна const handleClose = useCallback(() => {
document.body.classList.remove('no-scroll'); // Удаление класса "no-scroll" с тела документа document.body.classList.remove('no-scroll');
togglePopupMap(); // Вызов функции для закрытия всплывающего окна togglePopupMap();
}; }, [togglePopupMap]);
useEffect(() => { // Эффект для добавления класса "no-scroll" с тела документа при монтировании компонента useEffect(() => {
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
return () => { return () => {
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
@@ -51,7 +50,13 @@ export default function PopupMap({ togglePopupMap }: PopupMapProps) {
Курьером Курьером
</motion.button> </motion.button>
</div> </div>
<input type="search" name="address-search" id="address-search" placeholder="Искать на карте" className="menu-div__search-input" /> <input
type="search"
name="address-search"
id="address-search"
placeholder="Искать на карте"
className="menu-div__search-input"
/>
</div> </div>
<motion.button <motion.button
className="menu-div__select-button" className="menu-div__select-button"
@@ -62,8 +67,18 @@ export default function PopupMap({ togglePopupMap }: PopupMapProps) {
</motion.button> </motion.button>
</div> </div>
<div className="popup-map__map-div"> <div className="popup-map__map-div">
<a href="https://yandex.ru/maps/65/novosibirsk/?utm_medium=mapframe&utm_source=maps" style={{ color: "#eee", fontSize: "12px", position: "absolute", top: "0px" }}>Новосибирск</a> <a
<a href="https://yandex.ru/maps/65/novosibirsk/house/ulitsa_titova_14/bEsYfg9iSkEGQFtufXV5cn9lYQ==/?ll=82.882443%2C54.983268&utm_medium=mapframe&utm_source=maps&z=18.59" style={{ color: "#eee", fontSize: "12px", position: "absolute", top: "14px" }}>Улица Титова, 14 Яндекс Карты</a> href="https://yandex.ru/maps/65/novosibirsk/?utm_medium=mapframe&utm_source=maps"
style={{ color: "#eee", fontSize: "12px", position: "absolute", top: "0px" }}
>
Новосибирск
</a>
<a
href="https://yandex.ru/maps/65/novosibirsk/house/ulitsa_titova_14/bEsYfg9iSkEGQFtufXV5cn9lYQ==/?ll=82.882443%2C54.983268&utm_medium=mapframe&utm_source=maps&z=18.59"
style={{ color: "#eee", fontSize: "12px", position: "absolute", top: "14px" }}
>
Улица Титова, 14 Яндекс Карты
</a>
<iframe <iframe
title="map" title="map"
src="https://yandex.ru/map-widget/v1/?ll=82.882443%2C54.983268&mode=search&ol=geo&ouri=ymapsbm1%3A%2F%2Fgeo%3Fdata%3DCgg1NzA5NDgyMhJB0KDQvtGB0YHQuNGPLCDQndC-0LLQvtGB0LjQsdC40YDRgdC6LCDRg9C70LjRhtCwINCi0LjRgtC-0LLQsCwgMTQiCg3Dw6VCFffuW0I%2C&z=18.59" src="https://yandex.ru/map-widget/v1/?ll=82.882443%2C54.983268&mode=search&ol=geo&ouri=ymapsbm1%3A%2F%2Fgeo%3Fdata%3DCgg1NzA5NDgyMhJB0KDQvtGB0YHQuNGPLCDQndC-0LLQvtGB0LjQsdC40YDRgdC6LCDRg9C70LjRhtCwINCi0LjRgtC-0LLQsCwgMTQiCg3Dw6VCFffuW0I%2C&z=18.59"
@@ -76,4 +91,6 @@ export default function PopupMap({ togglePopupMap }: PopupMapProps) {
</div> </div>
</> </>
); );
} }
export default PopupMap;

View File

@@ -2,7 +2,7 @@ import React from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Product } from "../utils/types"; import { Product } from "../utils/types";
export default function ProductCard({ icons, title, price }: Product) { function ProductCard({ icons, title, price }: Product) {
const priceAsString = price.toLocaleString('ru-RU'); const priceAsString = price.toLocaleString('ru-RU');
return( return(
@@ -22,4 +22,6 @@ export default function ProductCard({ icons, title, price }: Product) {
</h6> </h6>
</motion.article> </motion.article>
) )
} }
export default ProductCard;

View File

@@ -3,7 +3,7 @@ import ProfileAvatar from '../assets/icons/profile-avatar.svg';
import '../ProfileStyle.scss'; import '../ProfileStyle.scss';
import Cookies from "js-cookie"; import Cookies from "js-cookie";
export default function ProfileInfo() { function ProfileInfo() {
const userLogin = Cookies.get('user'); const userLogin = Cookies.get('user');
return( return(
@@ -12,4 +12,6 @@ export default function ProfileInfo() {
<span>{userLogin || 'Гость'}</span> <span>{userLogin || 'Гость'}</span>
</div> </div>
) )
} }
export default ProfileInfo;

View File

@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
import '../ProfileStyle.scss'; import '../ProfileStyle.scss';
import ProfileInfo from "./ProfileInfo"; import ProfileInfo from "./ProfileInfo";
export default function ProfileOrders() { function ProfileOrders() {
return( return(
<section className="orders-section"> <section className="orders-section">
<nav className="profile-page__nav"> <nav className="profile-page__nav">
@@ -37,4 +37,6 @@ export default function ProfileOrders() {
</div> </div>
</section> </section>
) )
} }
export default ProfileOrders;

View File

@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
import '../ProfileStyle.scss'; import '../ProfileStyle.scss';
import ProfileInfo from "./ProfileInfo"; import ProfileInfo from "./ProfileInfo";
export default function ProfilePurchases() { function ProfilePurchases() {
return( return(
<section className="purchases-section"> <section className="purchases-section">
<nav className="profile-page__nav"> <nav className="profile-page__nav">
@@ -22,4 +22,6 @@ export default function ProfilePurchases() {
</div> </div>
</section> </section>
) )
} }
export default ProfilePurchases

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useCallback } from "react";
import axios from "axios"; import axios from "axios";
import { Reviews } from "../utils/types"; import { Reviews } from "../utils/types";
import "../index.scss"; import "../index.scss";
@@ -8,23 +8,39 @@ type ReviewProps = {
review: Reviews; review: Reviews;
}; };
export default function Review({ review }: ReviewProps) { function Review({ review }: ReviewProps) {
const [userName, setUserName] = useState<string>(""); // Состояние для имени пользователя const [userName, setUserName] = useState<string>("");
const readableDate = new Date(review.date).toLocaleDateString('ru-RU'); // Преобразование даты в читабельную форму const readableDate = new Date(review.date).toLocaleDateString('ru-RU');
useEffect(() => { // Получение имени пользователя по его ID useEffect(() => {
const baseUrl = window.location.origin; // Получаем текущий домен сайта const fetchUserName = async () => {
try {
axios.get(`${baseUrl}/api/get/user/${review.user_id}`) const baseUrl = window.location.origin;
.then(response => { const response = await axios.get(`${baseUrl}/api/get/user/${review.user_id}`);
const user = response.data.user[0]; const user = response.data.user[0];
setUserName(user.login); setUserName(user.login);
}) } catch (error) {
.catch(error => {
console.error('Ошибка при получении логина пользователя:', error); console.error('Ошибка при получении логина пользователя:', error);
}); }
};
fetchUserName();
}, [review.user_id]); }, [review.user_id]);
const renderStars = useCallback(() => {
return [1, 2, 3, 4, 5].map(rate => (
<input
key={rate}
type="radio"
className="star-rate__star-radio"
value={rate}
aria-label={rate === 1 ? "Плохо" : rate === 2 ? "Удовлетворительно" : rate === 3 ? "Нормально" : rate === 4 ? "Хорошо" : "Отлично"}
checked={review.rate === rate}
readOnly
/>
));
}, [review.rate]);
return ( return (
<article className="review-article"> <article className="review-article">
<div className="review-article__review-container"> <div className="review-article__review-container">
@@ -34,17 +50,7 @@ export default function Review({ review }: ReviewProps) {
</div> </div>
<div className="review-container__review-info"> <div className="review-container__review-info">
<div className="review-info__star-rate"> <div className="review-info__star-rate">
{[1, 2, 3, 4, 5].map(rate => ( {renderStars()}
<input
key={rate}
type="radio"
className="star-rate__star-radio"
value={rate}
aria-label={rate === 1 ? "Плохо" : rate === 2 ? "Удовлетворительно" : rate === 3 ? "Нормально" : rate === 4 ? "Хорошо" : "Отлично"}
checked={review.rate === rate}
readOnly
/>
))}
</div> </div>
<time className="review-info__review-date" dateTime={new Date(review.date).toISOString()}> <time className="review-info__review-date" dateTime={new Date(review.date).toISOString()}>
{readableDate} {readableDate}
@@ -55,4 +61,6 @@ export default function Review({ review }: ReviewProps) {
{review.icons && <img className="review-article__product-image" src={review.icons} alt="Review product" />} {review.icons && <img className="review-article__product-image" src={review.icons} alt="Review product" />}
</article> </article>
); );
} }
export default Review;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import '../ProductStyle.scss'; import '../ProductStyle.scss';
import ImageAttachIcon from "../assets/icons/review-form__add-image-icon.svg"; import ImageAttachIcon from "../assets/icons/review-form__add-image-icon.svg";
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
@@ -11,47 +11,47 @@ interface ReviewState {
image?: string | ArrayBuffer | null; image?: string | ArrayBuffer | null;
} }
export default function ReviewForm({ productId }: { productId: string }) { function ReviewForm({ productId }: { productId: string }) {
const [review, setReview] = useState<ReviewState>({ text: '', rating: 1 }); // Состояние для отзыва const [review, setReview] = useState<ReviewState>({ text: '', rating: 1 });
const [userId, setUserId] = useState<string | null>(null); // Состояние для ID пользователя const [userId, setUserId] = useState<string | null>(null);
const [imageName, setImageName] = useState<string | null>(null); // Состояние для имени изображения const [imageName, setImageName] = useState<string | null>(null);
useEffect(() => { // Получение ID пользователя из cookie при инициализации компонента useEffect(() => {
const userIdFromCookie = Cookies.get('user_id'); const userIdFromCookie = Cookies.get('user_id');
if (userIdFromCookie) { if (userIdFromCookie) {
setUserId(userIdFromCookie); setUserId(userIdFromCookie);
} }
}, []); }, []);
function handleTextChange(event: React.ChangeEvent<HTMLTextAreaElement>) { // Обработчик изменения текста отзыва const handleTextChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
setReview({ ...review, text: event.target.value }); setReview(prev => ({ ...prev, text: event.target.value }));
} }, []);
function handleRatingChange(event: React.ChangeEvent<HTMLInputElement>) { // Обработчик изменения оценки отзыва const handleRatingChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setReview({ ...review, rating: Number(event.target.value) }); setReview(prev => ({ ...prev, rating: Number(event.target.value) }));
} }, []);
function handleImageChange(event: React.ChangeEvent<HTMLInputElement>) { // Обработчик изменения изображения const handleImageChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files[0]) { if (event.target.files && event.target.files[0]) {
const file = event.target.files[0]; const file = event.target.files[0];
setImageName(file.name); setImageName(file.name);
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
setReview({ ...review, image: reader.result }); setReview(prev => ({ ...prev, image: reader.result }));
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
} }, []);
async function handleSubmit(event: React.FormEvent) { // Обработчик отправки формы const handleSubmit = useCallback(async (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();
if (!userId) { if (!userId) {
console.error('ID пользователя не найден!'); console.error('ID пользователя не найден!');
return; return;
} }
const baseUrl = window.location.origin; // Получаем текущий домен сайта const baseUrl = window.location.origin;
try { try {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -73,13 +73,12 @@ export default function ReviewForm({ productId }: { productId: string }) {
} catch (error) { } catch (error) {
console.error('Ошибка при отправке отзыва:', error); console.error('Ошибка при отправке отзыва:', error);
} }
} }, [review, userId, productId]);
return ( return (
<form className='product-page__review-form' onSubmit={handleSubmit}> <form className='product-page__review-form' onSubmit={handleSubmit}>
<h5 className='review-form__heading'>Оставить отзыв</h5> <h5 className='review-form__heading'>Оставить отзыв</h5>
<div className="review-form__stars-container"> <div className="review-form__stars-container">
{/* Создание радиокнопок для выбора оценки */}
{[...Array(5)].map((_, index) => ( {[...Array(5)].map((_, index) => (
<input <input
key={index} key={index}
@@ -108,7 +107,6 @@ export default function ReviewForm({ productId }: { productId: string }) {
<input <input
className='review-form__image-input' className='review-form__image-input'
type="file" type="file"
name="review image"
id="review-image" id="review-image"
accept='.png, .jpg, .jpeg' accept='.png, .jpg, .jpeg'
onChange={handleImageChange} onChange={handleImageChange}
@@ -124,5 +122,7 @@ export default function ReviewForm({ productId }: { productId: string }) {
Отправить отзыв Отправить отзыв
</motion.button> </motion.button>
</form> </form>
) );
} }
export default ReviewForm;

View File

@@ -9,7 +9,7 @@ type HomePageProps = {
products: Product[]; products: Product[];
} }
export default function HomePage({ products }: HomePageProps) { function HomePage({ products }: HomePageProps) {
return( return(
<section className="home-page"> <section className="home-page">
<Banner /> <Banner />
@@ -30,4 +30,6 @@ export default function HomePage({ products }: HomePageProps) {
</div> </div>
</section> </section>
); );
} }
export default HomePage;

View File

@@ -4,7 +4,7 @@ import "../InfoPageStyle.scss";
import RailTHAvatar from "../assets/img/info-page__railth-avatar.png"; import RailTHAvatar from "../assets/img/info-page__railth-avatar.png";
import NoKesspenAvatar from "../assets/img/info-page__no-kesspen-avatar.png"; import NoKesspenAvatar from "../assets/img/info-page__no-kesspen-avatar.png";
export default function InfoPage() { function InfoPage() {
return ( return (
<section className="info-page"> <section className="info-page">
<DevCard <DevCard
@@ -21,4 +21,6 @@ export default function InfoPage() {
/> />
</section> </section>
); );
} }
export default InfoPage;

View File

@@ -1,59 +1,67 @@
import React, { useState } from "react"; import React, { useState, useCallback } from "react";
import '../PaymentStyle.scss'; import '../PaymentStyle.scss';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
export default function PaymentPage() { function PaymentPage() {
const [ccNumber, setCcNumber] = useState(""); // Состояние для номера карты const [ccNumber, setCcNumber] = useState(""); // Состояние для номера карты
const [valueDate, setValueDate] = useState<number | ''>(''); // Состояние для даты истечения срока действия карты const [expiryDate, setExpiryDate] = useState(""); // Состояние для даты истечения срока действия карты
const [valueCode, setValueCode] = useState<number | ''>(''); // Состояние для кода карты const [cvv, setCvv] = useState(""); // Состояние для кода карты
const location = useLocation(); // Получение параметров из URL const location = useLocation(); // Получение параметров из URL
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
const price = queryParams.get('price'); // Получение стоимости из URL const price = queryParams.get('price'); // Получение стоимости из URL
const formatAndSetCcNumber = (e: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения номера карты const formatAndSetCcNumber = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения номера карты
const inputVal = e.target.value.replace(/ /g, ""); // Удаление пробелов из введенного значения const inputVal = e.target.value.replace(/\D/g, ""); // Удаление всех символов, кроме цифр
let inputNumbersOnly = inputVal.replace(/\D/g, ""); // Удаление всех символов, кроме цифр
if (inputNumbersOnly.length > 16) { // Если введенное значение превышает 16 символов const formattedNumber = inputVal.match(/.{1,4}/g)?.join(" ") || ""; // Разделяем введенное значение на группы по 4 символа и добавляем пробелы между ними
inputNumbersOnly = inputNumbersOnly.substr(0, 16); // Усекаем его до 16 символов setCcNumber(formattedNumber); // Устанавливаем введенное значение в состояние
} }, []);
const splits = inputNumbersOnly.match(/.{1,4}/g); // Разделяем введенное значение на группы по 4 символа const handleChangeExpiryDate = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения даты истечения срока действия карты
const inputVal = event.target.value.replace(/\D/g, ""); // Удаление всех символов, кроме цифр
setExpiryDate(inputVal.slice(0, 4)); // Ограничиваем значение до 4 символов
}, []);
let spacedNumber = ""; // Строка для хранения введенного значения с разделителями const handleChangeCvv = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения кода карты
if (splits) { // Если разделение прошло успешно const inputVal = event.target.value.replace(/\D/g, ""); // Удаление всех символов, кроме цифр
spacedNumber = splits.join(" "); // Добавляем пробелы между группами по 4 символа setCvv(inputVal.slice(0, 3)); // Ограничиваем значение до 3 символов
} }, []);
setCcNumber(spacedNumber); // Устанавливаем введенное значение в состояние
};
const handleChangeDate = (event: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения даты истечения срока действия карты
const inputValue = event.target.value; // Получаем введенное значение
if (inputValue.length <= 4) { // Если введенное значение содержит не больше 4 символов
setValueDate(inputValue === '' ? '' : Number(inputValue)); // Устанавливаем значение в состояние
}
};
const handleChangeCode = (event: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения кода карты
const inputValue = event.target.value; // Получаем введенное значение
if (inputValue.length <= 3) { // Если введенное значение содержит не больше 3 символов
setValueCode(inputValue === '' ? '' : Number(inputValue)); // Устанавливаем значение в состояние
}
};
return( return(
<section className="payment-page"> <section className="payment-page">
<h2 className="payment-page__price">{price} </h2> <h2 className="payment-page__price">{price} </h2>
<div className="payment-page__payment-card"> <div className="payment-page__payment-card">
<h3 className="payment-card__heading"> Оплата картой </h3> <h3 className="payment-card__heading">Оплата картой</h3>
<input className="payment-card__input" type="text" placeholder="Номер" value={ccNumber} onChange={formatAndSetCcNumber}/> <input
className="payment-card__input"
type="text"
placeholder="Номер"
value={ccNumber}
onChange={formatAndSetCcNumber}
maxLength={19} // Максимальная длина ввода (16 цифр + 3 пробела)
/>
<div className="payment-card__inputs-group"> <div className="payment-card__inputs-group">
<input className="payment-card__input" type="number" placeholder="ММ/ГГ" value={valueDate} onChange={handleChangeDate}/> <input
<input className="payment-card__input" type="number" placeholder="CVC/CVV" value={valueCode} onChange={handleChangeCode}/> className="payment-card__input"
type="text"
placeholder="ММГГ"
value={expiryDate}
onChange={handleChangeExpiryDate}
maxLength={4} // Максимальная длина ввода
/>
<input
className="payment-card__input"
type="text"
placeholder="CVC/CVV"
value={cvv}
onChange={handleChangeCvv}
maxLength={3} // Максимальная длина ввода
/>
</div> </div>
</div> </div>
<a href="scam" className="payment-page__pay-link"> Оплатить </a> <a href="scam" className="payment-page__pay-link">Оплатить</a>
</section> </section>
) )
} }
export default PaymentPage;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Product, Reviews } from '../utils/types'; import { Product, Reviews } from '../utils/types';
import Review from '../components/Review'; import Review from '../components/Review';
import axios from 'axios'; import axios from 'axios';
@@ -7,67 +7,51 @@ import ShareIcon from "../assets/icons/share-icon.svg";
import ReviewForm from '../components/ReviewForm'; import ReviewForm from '../components/ReviewForm';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
export default function ProductPage() { function ProductPage() {
const { id } = useParams(); // Получение id из URL-параметров const { id } = useParams(); // Получение id из URL-параметров
// Состояние для продукта и рецензий // Состояние для продукта и рецензий
const [product, setProduct] = useState<Product | null>(null); const [product, setProduct] = useState<Product | null>(null);
const [reviews, setReviews] = useState<Reviews[]>([]); const [reviews, setReviews] = useState<Reviews[]>([]);
// Состояние для среднего рейтинга и флага для отслеживания получения данных // Состояние для среднего рейтинга
const [averageRating, setAverageRating] = useState<number>(0); const [averageRating, setAverageRating] = useState<number>(0);
const [isDataFetched, setIsDataFetched] = useState(false);
const totalReviews = reviews.length; // Количество рецензий const fetchProductAndReviews = useCallback(async () => {
try {
const baseUrl = window.location.origin;
const [productResponse, reviewsResponse] = await Promise.all([
axios.get(`${baseUrl}/api/get/products`),
axios.get(`${baseUrl}/api/get/reviews/${id}`)
]);
const trimText = (text: string, limit: number): string => { // Функция для усечения текста const productData = productResponse.data.products.find((item: Product) => item.id.toString() === id);
return text.length > limit ? text.substring(0, limit) + '...' : text; setProduct(productData);
};
const countReviewsByRate = (rate: number): number => { // Функция для подсчета рецензий по рейтингу const reviewsData = reviewsResponse.data.review;
return reviews.filter(review => review.rate === rate).length; setReviews(reviewsData);
};
const percentageOfRate = (rate: number): number => { // Функция для вычисления процента рецензий по рейтингу const totalRating = reviewsData.reduce((acc: number, review: Reviews) => acc + review.rate, 0);
const count = countReviewsByRate(rate); setAverageRating(reviewsData.length > 0 ? totalRating / reviewsData.length : 0);
return (count / totalReviews) * 100; } catch (error) {
}; console.error('Ошибка при получении данных:', error);
}
useEffect(() => { // Получение продукта по его id
const baseUrl = window.location.origin; // Получаем текущий домен сайта
axios.get(`${baseUrl}/api/get/products`)
.then(response => {
const productData = response.data.products.find(
(item: Product) => item.id.toString() === id
);
setProduct(productData);
})
.catch(error => {
console.error('Ошибка при получении продукта:', error);
});
}, [id]); }, [id]);
useEffect(() => { // Получение рецензий по id продукта useEffect(() => {
if (!isDataFetched) { fetchProductAndReviews();
const baseUrl = window.location.origin; }, [fetchProductAndReviews]);
axios.get(`${baseUrl}/api/get/reviews/${id}`)
.then(response => {
const reviewsData = response.data.review;
setReviews(reviewsData);
const totalRating = reviewsData.reduce((acc: number, review: Reviews) => acc + review.rate, 0);
const average = totalRating / reviewsData.length;
setAverageRating(reviewsData.length > 0 ? average : 0);
setIsDataFetched(true);
})
.catch(error => {
console.error('Ошибка при получении рецензий:', error);
});
}
}, [id, isDataFetched]);
if (!product) { // Отображение загрузки, если продукт не загружен const trimText = (text: string, limit: number): string =>
text.length > limit ? text.substring(0, limit) + '...' : text;
const countReviewsByRate = useCallback((rate: number): number =>
reviews.filter(review => review.rate === rate).length, [reviews]);
const percentageOfRate = useCallback((rate: number): number =>
(countReviewsByRate(rate) / reviews.length) * 100, [countReviewsByRate, reviews.length]);
if (!product) {
return <div>Загрузка...</div>; return <div>Загрузка...</div>;
} }
@@ -106,16 +90,12 @@ export default function ProductPage() {
<div className='rate-block__rating'> <div className='rate-block__rating'>
<span className='rate-block__rate-number'>{averageRating.toFixed(1)}</span> <span className='rate-block__rate-number'>{averageRating.toFixed(1)}</span>
<div className="rate-block__star-rating"> <div className="rate-block__star-rating">
{/* Контейнер для отображения звезд, занимающий 100% ширины */}
<div className="star-rating__back-stars"> <div className="star-rating__back-stars">
{/* Отображение звезд, которые не должны быть закрашены */}
{'★★★★★'.split('').map((star, i) => ( {'★★★★★'.split('').map((star, i) => (
<span key={`back-star-${i}`}>{star}</span> <span key={`back-star-${i}`}>{star}</span>
))} ))}
{/* Контейнер для отображения звезд, которые должны быть закрашены */}
<div className="star-rating__front-stars" <div className="star-rating__front-stars"
style={{ width: `${(averageRating / 5) * 100}%` }}> style={{ width: `${(averageRating / 5) * 100}%` }}>
{/* Отображение звезд, которые должны быть закрашены */}
{'★★★★★'.split('').map((star, i) => ( {'★★★★★'.split('').map((star, i) => (
<span key={`front-star-${i}`}>{star}</span> <span key={`front-star-${i}`}>{star}</span>
))} ))}
@@ -143,4 +123,6 @@ export default function ProductPage() {
</section> </section>
</section> </section>
); );
} }
export default ProductPage;

View File

@@ -8,7 +8,7 @@ import ProfilePurchases from "../components/ProfilePurchases";
import LogoutIcon from "../assets/icons/logout-icon.svg"; import LogoutIcon from "../assets/icons/logout-icon.svg";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
export default function ProfilePage() { function ProfilePage() {
const navigate = useNavigate(); const navigate = useNavigate();
const handleLogout = () => { const handleLogout = () => {
@@ -34,4 +34,6 @@ export default function ProfilePage() {
</motion.button> </motion.button>
</section> </section>
) )
} }
export default ProfilePage;

View File

@@ -2,10 +2,12 @@ import React from "react";
import '../ScamStyle.scss'; import '../ScamStyle.scss';
import ScamImage from "../assets/img/scam-image.png"; import ScamImage from "../assets/img/scam-image.png";
export default function ScamPage() { function ScamPage() {
return( return(
<section className="scam-page"> <section className="scam-page">
<img src={ScamImage} alt="scam mammoth" className="scam-page__image"/> <img src={ScamImage} alt="scam mammoth" className="scam-page__image"/>
</section> </section>
) )
} }
export default ScamPage;