15 Commits

Author SHA1 Message Date
sdarbinyan
caf14eeae1 added translations 2026-02-26 23:09:20 +04:00
sdarbinyan
e4206d8abc added language routing system 2026-02-26 22:23:08 +04:00
sdarbinyan
a4765ffe98 fixed header icons active state 2026-02-26 22:00:12 +04:00
sdarbinyan
10b4974719 optimising and making it better 2026-02-26 21:54:21 +04:00
sdarbinyan
7a00a8f1e3 changed legal 2026-02-24 21:24:33 +04:00
sdarbinyan
2baa72a022 fixed image and added priority 2026-02-20 00:44:44 +04:00
sdarbinyan
18df968b7a improvments are done 2026-02-19 01:23:25 +04:00
sdarbinyan
e3efb270dd styles 2026-02-19 00:55:03 +04:00
sdarbinyan
0692cc6360 request check removed 2026-02-14 20:18:55 +04:00
sdarbinyan
61f441f6b2 some style changes 2026-02-14 18:38:25 +04:00
sdarbinyan
9154660a01 mobile is finished 2026-02-14 02:59:26 +04:00
sdarbinyan
4238d59fc6 style changes 2026-02-14 02:34:11 +04:00
sdarbinyan
751ad48489 home page 2026-02-14 01:28:08 +04:00
sdarbinyan
88ac37ebc4 changed header and hero img 2026-02-14 00:45:17 +04:00
sdarbinyan
39290ef776 style chages 2026-01-23 00:34:16 +04:00
114 changed files with 5387 additions and 3735 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@
/tmp /tmp
/out-tsc /out-tsc
/bazel-out /bazel-out
/files
# Node # Node
/node_modules /node_modules

View File

@@ -146,7 +146,8 @@
}, },
"sourceMap": false, "sourceMap": false,
"namedChunks": false, "namedChunks": false,
"extractLicenses": true "extractLicenses": true,
"serviceWorker": "ngsw-config.json"
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"

500
files/changes.txt Normal file
View File

@@ -0,0 +1,500 @@
we ae going to redesing dexar. here are css from the figma. i will try to explain all.
pls do responsive and better! thank you
you are free to do changes better and responsive ofc!!
Header:
<div class="frame">
<img class="group" src="img/group-2.png" />
<div class="div">
<div class="div-wrapper"><div class="text-wrapper">Главная</div></div>
<div class="div-wrapper-2"><div class="text-wrapper">О нас</div></div>
<div class="div-wrapper-3"><div class="text-wrapper-2">Контакты</div></div>
</div>
<div class="frame-wrapper">
<div class="div-2">
<div class="text-wrapper-3">Искать...</div>
<img class="icn" src="img/icn-05.png" />
</div>
</div>
<div class="korzina-frame"><img class="cart" src="img/cart.svg" /></div>
<div class="RU-frame">
<div class="text-wrapper-4">RU</div>
<div class="group-2"><img class="line" src="img/line-2.svg" /> <img class="img" src="img/line-3.svg" /></div>
</div>
<div class="login-frame"><img class="icon" src="img/icon.svg" /></div>
</div>
.frame {
width: 1440px;
height: 84px;
display: flex;
background-color: #74787b1a;
}
.frame .group {
margin-top: 18px;
width: 148px;
height: 48px;
position: relative;
margin-left: 56px;
}
.frame .div {
display: inline-flex;
margin-top: 18px;
width: 569px;
height: 49px;
position: relative;
margin-left: 57px;
align-items: flex-start;
}
.frame .div-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 10px 48px;
position: relative;
flex: 0 0 auto;
background-color: #497671;
border-radius: 13px 0px 0px 13px;
border: 1px solid;
border-color: #d3dad9;
box-shadow: 0px 3px 4px #00000026;
}
.frame .text-wrapper {
position: relative;
width: fit-content;
margin-top: -1.00px;
font-family: "DM Sans-SemiBold", Helvetica;
font-weight: 600;
color: #ffffff;
font-size: 22px;
text-align: center;
letter-spacing: 0;
line-height: normal;
}
.frame .div-wrapper-2 {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 10px 63px;
position: relative;
flex: 0 0 auto;
background-color: #a1b4b5;
border: 1px solid;
border-color: #d3dad9;
box-shadow: 0px 3px 4px #00000026;
}
.frame .div-wrapper-3 {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 10px 42px;
position: relative;
flex: 0 0 auto;
background-color: #ffffffbd;
border-radius: 0px 13px 13px 0px;
border: 1px solid;
border-color: #d3dad9;
box-shadow: 0px 3px 4px #00000026;
}
.frame .text-wrapper-2 {
color: #1e3c38;
position: relative;
width: fit-content;
margin-top: -1.00px;
font-family: "DM Sans-SemiBold", Helvetica;
font-weight: 600;
font-size: 22px;
text-align: center;
letter-spacing: 0;
line-height: normal;
}
.frame .frame-wrapper {
margin-top: 18px;
width: 234px;
height: 49px;
position: relative;
margin-left: 126px;
background-color: #ffffffbd;
border-radius: 22px;
border: 1px solid;
border-color: #d2dad9;
box-shadow: 0px 3px 4px #00000026;
}
.frame .div-2 {
display: inline-flex;
align-items: center;
gap: 27px;
padding: 0px 20px;
position: relative;
top: 10px;
left: 50px;
}
.frame .text-wrapper-3 {
color: #828e8d;
position: relative;
width: fit-content;
margin-top: -1.00px;
font-family: "DM Sans-SemiBold", Helvetica;
font-weight: 600;
font-size: 22px;
text-align: center;
letter-spacing: 0;
line-height: normal;
}
.frame .icn {
position: absolute;
top: 1px;
left: -32px;
width: 28px;
height: 28px;
}
.frame .korzina-frame {
margin-top: 26px;
width: 48px;
height: 32px;
position: relative;
margin-left: 57px;
background-color: #ffffff4c;
border-radius: 12px;
border: 1px solid;
border-color: #667a77;
}
.frame .cart {
position: absolute;
top: calc(50.00% - 13px);
left: calc(50.00% - 14px);
width: 27px;
height: 27px;
}
.frame .RU-frame {
display: flex;
margin-top: 26px;
width: 67px;
height: 32px;
position: relative;
margin-left: 4px;
align-items: center;
gap: 8px;
padding: 6px;
background-color: #ffffff4c;
border-radius: 12px;
border: 1px solid;
border-color: #667a77;
}
.frame .text-wrapper-4 {
position: relative;
width: fit-content;
margin-top: -6.50px;
margin-bottom: -4.50px;
font-family: "DM Sans-Medium", Helvetica;
font-weight: 500;
color: #1e3c38;
font-size: 24px;
letter-spacing: 0;
line-height: normal;
}
.frame .group-2 {
position: relative;
width: 9.29px;
height: 14px;
transform: rotate(90.00deg);
}
.frame .line {
top: -2px;
position: absolute;
left: 1px;
width: 9px;
height: 10px;
transform: rotate(-90.00deg);
}
.frame .img {
top: 6px;
position: absolute;
left: 1px;
width: 9px;
height: 10px;
transform: rotate(-90.00deg);
}
.frame .login-frame {
margin-top: 26px;
width: 48px;
height: 32px;
position: relative;
margin-left: 4px;
background-color: #ffffff4c;
border-radius: 12px;
border: 1px solid;
border-color: #667a77;
}
.frame .icon {
position: absolute;
top: calc(50.00% - 12px);
left: calc(50.00% - 12px);
width: 24px;
height: 24px;
}
1. background: rgba(117, 121, 124, 0.1);
padding: 14px 0px;
width: 1440px;
height: 84px;
2. logo stays the
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_1" data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 308.43 100.53">
<defs>
<style>
.cls-1 {
fill: #477470;
stroke-width: 0px;
}
</style>
</defs>
<path class="cls-1" d="m101.66,15.71c-4.16-.3-8.34-.35-12.51-.46-3.85-.1-7.69-.15-11.54-.21-9.14-.15-18.29-.32-27.44-.44-7.84-.11-15.68-.18-23.53-.21-.83,0-1.17-.3-1.33-1.01-.81-3.51-1.64-7.02-2.44-10.53-.31-1.33-1.42-2.36-2.68-2.41-1.59-.07-3.18-.17-4.77-.21C11.37.13,7.31.06,3.25,0,1.27-.03,0,1.13,0,2.92c0,1.78,1.38,3.14,3.26,3.17,4.28.08,8.56.17,12.84.2.89,0,1.34.26,1.56,1.17,1.2,4.99,2.47,9.95,3.69,14.93,2.3,9.38,4.58,18.77,6.88,28.15,1.11,4.54,2.21,9.07,3.36,13.6.28,1.11.15,1.73-1.02,2.31-3.76,1.85-5.33,5.91-4.45,9.93.91,4.11,4.58,6.95,9.07,7.02.46,0,.92,0,1.38,0-2.97,1.75-4.68,4.13-4.95,7.42-.27,3.32,1.42,5.8,3.95,7.96-4.85.74-6.27.75-9.41,1.23.8.23,1.31.11,1.98.12,4.46.05,8.92.17,13.37.01,4.94-.17,8.86-5.16,7.57-10.63-.63-2.66-2.21-4.7-5.04-5.9h39.73c-2.87,1.74-4.53,4.14-4.85,7.36-.32,3.29,1.08,5.9,3.89,8.11-9.01.38-17.71.47-26.34,1.09l30.02.35c1.84-.07,3.73.03,5.49-.97,4.82-2.75,6.23-8.3,3.26-12.73-.84-1.26-2.17-2.19-3.21-3.2,1.3,0,2.83.03,4.35,0,1.66-.04,2.81-1.34,2.78-3.08-.02-1.56-1.25-2.77-2.82-2.79-6.68-.07-13.36-.18-20.04-.2-9.37-.04-18.74-.01-28.11-.02-4.25,0-8.5,0-12.75,0-2.17,0-3.72-1.47-3.62-3.37.09-1.79,1.73-3.16,3.83-3.15,8.39.04,16.77.1,25.16.13,8.61.04,17.21.06,25.82.07.97,0,1.94-.09,2.9-.21,3.83-.52,6.67-3.16,7.69-6.89,1.84-6.75,3.76-13.47,5.65-20.21,1.36-4.84,2.79-9.66,4.08-14.52.59-2.2,1.13-4.45,1.32-6.7.29-3.53-2.89-6.7-6.6-6.96Zm-13.8,71.86c2.2-.07,4.11,1.95,4.1,4.15-.18,2.67-1.84,3.97-4.24,4.07-2.17.08-4.06-1.98-4.03-4.18.03-2.3,1.72-3.96,4.17-4.04Zm-47.43-.03c2.45-.06,4.19,1.8,4.15,4.03-.05,2.63-2.02,3.98-4.06,4.02-2.23.04-4.05-1.86-4.15-4.07-.1-2.22,2.05-4.07,4.06-3.98Zm30.45-67.01v12.33c-1.89,0-3.69.02-5.48,0-3.15-.05-6.3-.18-9.45-.18-.98,0-1.2-.35-1.27-1.24-.22-2.76-.55-5.5-.82-8.25-.09-.93-.15-1.86-.21-2.66h17.23Zm-.14,17.64v12.64c-4.47,0-8.88.02-13.29-.04-.26,0-.71-.63-.75-1.01-.35-3.18-.62-6.37-.91-9.55,0-.04,0-.07,0-.11-.15-1.98-.15-1.95,1.83-1.94,4.35.02,8.69,0,13.13,0Zm-41.31-8.1c-.62-2.71-1.26-5.41-1.88-8.12-.15-.65-.27-1.32-.43-2.1,7.05.12,13.97.24,21.04.37.41,4.15.81,8.23,1.19,12.14-5.73,0-11.3,0-16.87,0-.11,0-.22-.02-.32-.03-2.25-.14-2.24-.14-2.73-2.26Zm5.02,20.67c-1.01-4.24-2.02-8.49-3.03-12.7h18.64c.47,4.3.93,8.46,1.39,12.7h-17.01Zm57.74,8.57c-.3,1.1-.54,2.23-.89,3.31-.51,1.58-1.87,2.54-3.47,2.54-16.08-.01-32.17-.04-48.25,0-1.26,0-1.71-.36-1.95-1.57-.44-2.27-1.1-4.5-1.65-6.75-.04-.17,0-.35,0-.67,18.95.13,37.85.26,56.99.39-.29,1.03-.53,1.89-.77,2.76Zm4.75-16.54c-.7,2.51-1.41,5.02-2.17,7.51-.09.29-.56.65-.85.65-5.59.04-11.18.04-16.77,0-.29,0-.83-.42-.84-.64-.05-3.87-.04-7.75-.04-11.6h21.71c-.38,1.5-.69,2.8-1.05,4.08Zm5.38-19.31c-.83,2.95-1.7,5.89-2.49,8.85-.19.73-.47,1.01-1.23.99-6.45-.16-12.91-.28-19.36-.41-.94-.02-1.88,0-2.97,0,0-3.91.01-7.67,0-11.43,0-.76.45-.78,1-.77,2.83.08,5.65.17,8.48.22,4.93.09,9.86.15,14.79.22,1.49.02,2.18.94,1.78,2.34Z"/>
<path class="cls-1" d="m299.48,39.67c.17-.09.36-.18.54-.28,3.09-1.58,5.27-3.86,5.99-7.4.42-2.08.51-4.14.17-6.22-.51-3.09-1.95-5.6-4.74-7.19-2.92-1.67-6.16-2.13-9.43-2.22-4.54-.13-9.08-.02-13.62-.04-.68,0-.98.18-.98.92.02,11.58.02,23.15,0,34.73,0,.72.26.96.96.95,1.71-.03,3.41-.03,5.12.02.85.03,1.15-.26,1.14-1.12-.04-3.23-.02-6.46-.02-9.69v-1.18c2.28,0,4.38.04,6.48-.02.77-.02,1.18.27,1.57.87,1.95,3.04,4,6.02,5.85,9.11.89,1.49,1.85,2.24,3.68,2.06,1.95-.2,3.94-.04,6.23-.04-3.09-4.57-6.01-8.89-8.95-13.25Zm-.65-8.49c-.41,1.92-1.85,2.99-3.63,3.16-3.3.31-6.64.33-9.96.42-.2,0-.59-.48-.59-.74-.04-3.81-.03-7.61-.03-11.8,3.68.22,7.25.24,10.77.71,2.49.33,3.8,2.22,3.81,4.75,0,1.17-.13,2.36-.37,3.51Z"/>
<path class="cls-1" d="m160.88,43.32c2.31-4.64,2.45-9.55,1.34-14.5-.78-3.47-2.57-6.41-5.35-8.65-3.79-3.05-8.3-4.12-13.04-4.26-3.99-.11-7.99.01-11.98-.05-1.08-.02-1.33.33-1.33,1.36.03,11.35.02,22.71.02,34.06v1.2c3.27,0,6.38.06,9.5-.02,2.92-.07,5.87-.03,8.73-.48,5.42-.85,9.62-3.66,12.11-8.67Zm-5.96-4c-1.11,3.56-4.21,6.16-7.89,6.59-2.68.32-5.41.24-8.12.41-.96.06-1.17-.33-1.16-1.19.03-3.66.01-7.32.01-10.99.02,0,.03,0,.05,0,0-3.7-.01-7.4.02-11.09,0-.28.34-.81.52-.81,3.16.01,6.35-.32,9.47.56,4.39,1.24,6.86,4.16,7.57,8.62.43,2.66.34,5.3-.47,7.88Z"/>
<path class="cls-1" d="m176.08,37.91c0-.65.38-.66.86-.65,3.92.06,7.84.12,11.76.16,1.36.02,2.72,0,4.17,0,0-1.95-.04-3.62.02-5.28.03-.84-.28-1.03-1.07-1.02-4.83.03-9.66.02-14.49.02h-1.27c0-2.91-.01-5.7.03-8.48,0-.17.43-.48.66-.48,5.15-.02,10.31-.01,15.46-.01.47,0,.94-.05,1.42-.03.73.04,1.03-.22,1-1-.06-1.27-.07-2.54,0-3.81.06-.94-.22-1.25-1.2-1.24-7.04.03-14.09,0-21.13,0-1.11,0-2.22,0-3.31,0v36.58h25.96v-6.21h-18.86c0-2.98,0-5.76,0-8.55Z"/>
<path class="cls-1" d="m265.06,35c-2.49-6.04-4.99-12.08-7.52-18.1-.12-.28-.65-.53-1-.54-1.92-.05-3.85,0-5.77-.04-.7-.02-1,.27-1.26.89-2.73,6.57-5.49,13.12-8.23,19.68-2.17,5.21-4.32,10.42-6.61,15.95,2.43,0,4.65.03,6.86-.04.34-.01.81-.44.96-.79.93-2.17,1.76-4.38,2.69-6.55.15-.34.61-.79.93-.79,4.94.01,9.87.11,14.81.13.67,0,.84.31,1.04.81.86,2.16,1.73,4.31,2.63,6.45.11.26.38.65.59.65,2.34.05,4.68.03,7.12.03-.11-.33-.19-.63-.31-.91-2.3-5.62-4.6-11.23-6.91-16.84Zm-17.29,3.48c1.91-4.7,3.81-9.35,5.79-14.21,1.96,4.85,3.84,9.48,5.76,14.21h-11.54Z"/>
<path class="cls-1" d="m225.35,52.65c2.59.09,5.19.05,7.88.05-.08-.32-.09-.51-.18-.64-1.34-1.94-2.7-3.86-4.04-5.8-2.54-3.68-5.05-7.38-7.59-11.06-.54-.78-.8-1.41-.12-2.37,2.6-3.69,5.06-7.47,7.59-11.21,1.18-1.74,2.4-3.46,3.72-5.35-.47-.07-.71-.13-.95-.13-2.11,0-4.21-.06-6.32.03-.52.02-1.21.36-1.51.77-1.3,1.77-2.49,3.62-3.72,5.43-1.3,1.92-2.61,3.85-3.96,5.84-.26-.31-.43-.49-.57-.7-2.13-3.22-4.31-6.4-6.36-9.67-.79-1.26-1.63-1.88-3.2-1.76-2.04.17-4.09.04-6.28.04.14.36.18.57.29.73,3.71,5.4,7.42,10.8,11.15,16.19.43.62.42,1.09-.02,1.72-3.29,4.7-6.54,9.42-9.8,14.14-.83,1.21-1.63,2.45-2.53,3.81,2.74,0,5.24.02,7.74-.02.31,0,.73-.26.92-.53,2.4-3.49,4.77-7,7.15-10.51.45-.67.9-1.34,1.38-2.05,2.79,4.08,5.5,8.05,8.23,12,.29.42.72,1.05,1.1,1.06Z"/>
<path class="cls-1" d="m141.52,77.32l-1.21,2.83h-.11l-1.21-2.83-3.33-7.36h-3.58v14.94h2.99v-6.83c0-1.39-.25-3.38-.4-4.75h.11l1.47,3.4,3.19,6.78h1.5l3.19-6.78,1.5-3.4h.11c-.17,1.37-.42,3.36-.42,4.75v6.83h3.08v-14.94h-3.61l-3.24,7.36Z"/>
<path class="cls-1" d="m162.26,69.96l-6.04,14.94h3.36l1.44-4.04h6.18l1.44,4.04h3.47l-6.01-14.94h-3.84Zm-.51,8.82l.65-1.83c.59-1.58,1.13-3.27,1.64-4.93h.11c.54,1.64,1.1,3.36,1.66,4.93l.65,1.83h-4.71Z"/>
<path class="cls-1" d="m192.96,74.39c0-3.34-2.96-4.43-6.8-4.43h-6.21v14.94h3.27v-5.85h2.79l3.98,5.85h3.67l-4.4-6.24c2.23-.62,3.7-1.99,3.7-4.27Zm-7.14,2.56h-2.6v-4.87h2.6c2.54,0,3.89.59,3.89,2.31s-1.35,2.56-3.89,2.56Z"/>
<polygon class="cls-1" points="215.96 69.96 212.34 69.96 205.77 76.75 205.69 76.75 205.69 69.96 202.41 69.96 202.41 84.9 205.69 84.9 205.69 80.54 208.34 77.87 213.3 84.9 216.92 84.9 210.29 75.79 215.96 69.96"/>
<polygon class="cls-1" points="228.09 78.25 234.72 78.25 234.72 76.01 228.09 76.01 228.09 72.2 235.9 72.2 235.9 69.96 224.82 69.96 224.82 84.9 236.19 84.9 236.19 82.66 228.09 82.66 228.09 78.25"/>
<polygon class="cls-1" points="243.92 72.2 249.25 72.2 249.25 84.9 252.52 84.9 252.52 72.2 257.83 72.2 257.83 69.96 243.92 69.96 243.92 72.2"/>
</svg>
3. after logo 3 btns in same div and without gap
3.1 "главная"
border: 1px solid #d3dad9;
border-radius: 13px 0 0 13px;
padding: 10px 48px;
width: 187px;
height: 49px;
3.2 "о нас"border:
1px solid #d3dad9;
padding: 10px 63px;
width: 188px;
height: 49px;
3.3 "котакты"border:
1px solid #d3dad9;
border-radius: 0 13px 13px 0;
padding: 10px 42px;
width: 194px;
height: 49px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 0.74);
hover: background: #a1b4b5;
active : background: #497671;
4. next search btn with place holder "искать..." and on the left fixed svg icon "<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" fill="#576463" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#576463" />
</svg>"
border: 1px solid #d3dad9;
border-radius: 22px;
padding: 6px 10px;
width: 234px;
height: 49px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 0.74);
5. after 3 buttons to the right
5.1 cart btn
border-radius: 12px;
fill: rgba(255, 255, 255, 0.3);
border: 1px solid #677b78;
<svg width="48" height="32" viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0.5H36C42.3513 0.5 47.5 5.64873 47.5 12V20C47.5 26.3513 42.3513 31.5 36 31.5H12C5.64873 31.5 0.5 26.3513 0.5 20V12C0.5 5.64873 5.64873 0.5 12 0.5Z" fill="white" fill-opacity="0.3" />
<path d="M12 0.5H36C42.3513 0.5 47.5 5.64873 47.5 12V20C47.5 26.3513 42.3513 31.5 36 31.5H12C5.64873 31.5 0.5 26.3513 0.5 20V12C0.5 5.64873 5.64873 0.5 12 0.5Z" stroke="#677B78" />
<path d="M10 3.9C10 3.40294 10.4029 3 10.9 3H13.6C14.013 3 14.373 3.28107 14.4731 3.68172L15.2027 6.6H36.1C36.3677 6.6 36.6216 6.7192 36.7925 6.92523C36.9635 7.13125 37.0339 7.40271 36.9846 7.66586L34.2846 22.0659C34.2048 22.4915 33.8331 22.8 33.4 22.8H31.6H19H17.2C16.7669 22.8 16.3952 22.4915 16.3154 22.0659L13.6204 7.69224L12.8973 4.8H10.9C10.4029 4.8 10 4.39706 10 3.9ZM15.5844 8.4L17.9469 21H32.6531L35.0156 8.4H15.5844ZM19 22.8C17.0118 22.8 15.4 24.4118 15.4 26.4C15.4 28.3882 17.0118 30 19 30C20.9882 30 22.6 28.3882 22.6 26.4C22.6 24.4118 20.9882 22.8 19 22.8ZM31.6 22.8C29.6118 22.8 28 24.4118 28 26.4C28 28.3882 29.6118 30 31.6 30C33.5882 30 35.2 28.3882 35.2 26.4C35.2 24.4118 33.5882 22.8 31.6 22.8ZM19 24.6C19.9941 24.6 20.8 25.4059 20.8 26.4C20.8 27.3941 19.9941 28.2 19 28.2C18.0059 28.2 17.2 27.3941 17.2 26.4C17.2 25.4059 18.0059 24.6 19 24.6ZM31.6 24.6C32.5941 24.6 33.4 25.4059 33.4 26.4C33.4 27.3941 32.5941 28.2 31.6 28.2C30.6059 28.2 29.8 27.3941 29.8 26.4C29.8 25.4059 30.6059 24.6 31.6 24.6Z" fill="#1E3C38" />
</svg>
5.2 lang selector btn style border: 1px solid #677b78;
border-radius: 12px;
padding: 6px;
width: 67px;
height: 32px;
HERO
we are goung to have a width wide hero, photos for dekstop and mobile you can see in the same folder
on it text. here are codes from figma
<div class="frame">
<div class="text-wrapper">Здесь ты найдёшь всё</div>
<p class="div">Тысячи товаров в одном месте</p>
<div class="text-wrapper-2">просто и удобно</div>
</div>
.frame {
display: flex;
flex-direction: column;
width: 639px;
align-items: flex-start;
gap: 18px;
position: relative;
}
.frame .text-wrapper {
position: relative;
width: 659px;
margin-top: -1.00px;
margin-right: -20.00px;
font-size: 57px;
font-family: "DM Sans-Medium", Helvetica;
font-weight: 500;
color: #1e3c38;
letter-spacing: 0;
line-height: normal;
}
.frame .div {
position: absolute;
top: 87px;
left: 0;
width: 581px;
font-size: 34px;
font-family: "DM Sans-Medium", Helvetica;
font-weight: 500;
color: #1e3c38;
letter-spacing: 0;
line-height: normal;
}
.frame .text-wrapper-2 {
position: absolute;
top: 133px;
left: 0;
width: 281px;
font-size: 34px;
font-family: "DM Sans-Medium", Helvetica;
font-weight: 500;
color: #1e3c38;
letter-spacing: 0;
line-height: normal;
}
under the text we have btns.. hovers and actives for all web site are the same as from header
first
<div class="pereyti-v-katalog"><div class="text-wrapper">Перейти в каталог</div></div>
.pereyti-v-katalog {
width: 337px;
height: 60px;
display: flex;
border-radius: 13px;
border: 1px solid;
border-color: #d3dad9;
background: linear-gradient(
360deg,
rgba(73, 118, 113, 1) 0%,
rgba(167, 206, 202, 1) 100%
);
}
.pereyti-v-katalog .text-wrapper {
margin-top: 12px;
width: 269px;
height: 36px;
margin-left: 34px;
position: relative;
font-family: "DM Sans-Medium", Helvetica;
font-weight: 500;
color: #ffffff;
font-size: 27px;
text-align: center;
letter-spacing: 1.08px;
line-height: normal;
}
second btn
<div class="frame">
<div class="text-wrapper">Найти товар</div>
<div class="group"><img class="line" src="img/line-2.svg" /> <img class="img" src="img/line-3.svg" /></div>
</div>
.frame {
width: 264px;
height: 60px;
display: flex;
gap: 9.2px;
background-color: #f5f5f5;
border-radius: 13px;
border: 1px solid;
border-color: #d3dad9;
}
.frame .text-wrapper {
margin-top: 12px;
width: 181px;
height: 36px;
position: relative;
margin-left: 36px;
font-family: "DM Sans-Medium", Helvetica;
font-weight: 500;
color: #1e3c38;
font-size: 27px;
text-align: center;
letter-spacing: 1.08px;
line-height: normal;
}
.frame .group {
margin-top: 22.0px;
width: 10.62px;
height: 16px;
position: relative;
}
.frame .line {
top: -1px;
width: 12px;
position: absolute;
left: 1px;
height: 10px;
}
.frame .img {
top: 7px;
width: 11px;
position: absolute;
left: 1px;
height: 10px;
}

View File

@@ -8,7 +8,6 @@
"resources": { "resources": {
"files": [ "files": [
"/favicon.ico", "/favicon.ico",
"/index.csr.html",
"/index.html", "/index.html",
"/manifest.webmanifest", "/manifest.webmanifest",
"/*.css", "/*.css",
@@ -48,7 +47,7 @@
"https://**/*.webp" "https://**/*.webp"
], ],
"cacheConfig": { "cacheConfig": {
"maxSize": 50, "maxSize": 200,
"maxAge": "7d", "maxAge": "7d",
"strategy": "performance" "strategy": "performance"
} }

View File

@@ -0,0 +1 @@
<svg data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108.43 100.53"><path d="M101.66 15.71c-4.16-.3-8.34-.35-12.51-.46-3.85-.1-7.69-.15-11.54-.21-9.14-.15-18.29-.32-27.44-.44-7.84-.11-15.68-.18-23.53-.21-.83 0-1.17-.3-1.33-1.01-.81-3.51-1.64-7.02-2.44-10.53-.31-1.33-1.42-2.36-2.68-2.41C18.6.37 17.01.27 15.42.23 11.37.13 7.31.06 3.25 0 1.27-.03 0 1.13 0 2.92 0 4.7 1.38 6.06 3.26 6.09c4.28.08 8.56.17 12.84.2.89 0 1.34.26 1.56 1.17 1.2 4.99 2.47 9.95 3.69 14.93 2.3 9.38 4.58 18.77 6.88 28.15 1.11 4.54 2.21 9.07 3.36 13.6.28 1.11.15 1.73-1.02 2.31-3.76 1.85-5.33 5.91-4.45 9.93.91 4.11 4.58 6.95 9.07 7.02h1.38c-2.97 1.75-4.68 4.13-4.95 7.42-.27 3.32 1.42 5.8 3.95 7.96-4.85.74-6.27.75-9.41 1.23.8.23 1.31.11 1.98.12 4.46.05 8.92.17 13.37.01 4.94-.17 8.86-5.16 7.57-10.63-.63-2.66-2.21-4.7-5.04-5.9h39.73c-2.87 1.74-4.53 4.14-4.85 7.36-.32 3.29 1.08 5.9 3.89 8.11-9.01.38-17.71.47-26.34 1.09l30.02.35c1.84-.07 3.73.03 5.49-.97 4.82-2.75 6.23-8.3 3.26-12.73-.84-1.26-2.17-2.19-3.21-3.2 1.3 0 2.83.03 4.35 0 1.66-.04 2.81-1.34 2.78-3.08-.02-1.56-1.25-2.77-2.82-2.79-6.68-.07-13.36-.18-20.04-.2-9.37-.04-18.74-.01-28.11-.02H35.44c-2.17 0-3.72-1.47-3.62-3.37.09-1.79 1.73-3.16 3.83-3.15 8.39.04 16.77.1 25.16.13 8.61.04 17.21.06 25.82.07.97 0 1.94-.09 2.9-.21 3.83-.52 6.67-3.16 7.69-6.89 1.84-6.75 3.76-13.47 5.65-20.21 1.36-4.84 2.79-9.66 4.08-14.52.59-2.2 1.13-4.45 1.32-6.7.29-3.53-2.89-6.7-6.6-6.96Zm-13.8 71.86c2.2-.07 4.11 1.95 4.1 4.15-.18 2.67-1.84 3.97-4.24 4.07-2.17.08-4.06-1.98-4.03-4.18.03-2.3 1.72-3.96 4.17-4.04m-47.43-.03c2.45-.06 4.19 1.8 4.15 4.03-.05 2.63-2.02 3.98-4.06 4.02-2.23.04-4.05-1.86-4.15-4.07-.1-2.22 2.05-4.07 4.06-3.98m30.45-67.01v12.33c-1.89 0-3.69.02-5.48 0-3.15-.05-6.3-.18-9.45-.18-.98 0-1.2-.35-1.27-1.24-.22-2.76-.55-5.5-.82-8.25-.09-.93-.15-1.86-.21-2.66zm-.14 17.64v12.64c-4.47 0-8.88.02-13.29-.04-.26 0-.71-.63-.75-1.01-.35-3.18-.62-6.37-.91-9.55v-.11c-.15-1.98-.15-1.95 1.83-1.94 4.35.02 8.69 0 13.13 0Zm-41.31-8.1c-.62-2.71-1.26-5.41-1.88-8.12-.15-.65-.27-1.32-.43-2.1 7.05.12 13.97.24 21.04.37.41 4.15.81 8.23 1.19 12.14H32.48c-.11 0-.22-.02-.32-.03-2.25-.14-2.24-.14-2.73-2.26m5.02 20.67c-1.01-4.24-2.02-8.49-3.03-12.7h18.64c.47 4.3.93 8.46 1.39 12.7H34.44Zm57.74 8.57c-.3 1.1-.54 2.23-.89 3.31-.51 1.58-1.87 2.54-3.47 2.54-16.08-.01-32.17-.04-48.25 0-1.26 0-1.71-.36-1.95-1.57-.44-2.27-1.1-4.5-1.65-6.75-.04-.17 0-.35 0-.67l56.99.39c-.29 1.03-.53 1.89-.77 2.76Zm4.75-16.54c-.7 2.51-1.41 5.02-2.17 7.51-.09.29-.56.65-.85.65q-8.385.06-16.77 0c-.29 0-.83-.42-.84-.64-.05-3.87-.04-7.75-.04-11.6h21.71c-.38 1.5-.69 2.8-1.05 4.08Zm5.38-19.31c-.83 2.95-1.7 5.89-2.49 8.85-.19.73-.47 1.01-1.23.99-6.45-.16-12.91-.28-19.36-.41-.94-.02-1.88 0-2.97 0 0-3.91.01-7.67 0-11.43 0-.76.45-.78 1-.77 2.83.08 5.65.17 8.48.22 4.93.09 9.86.15 14.79.22 1.49.02 2.18.94 1.78 2.34Z" style="fill:#477470;stroke-width:0"/></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 727 B

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="a" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#667eea;stop-opacity:1"/><stop offset="100%" style="stop-color:#764ba2;stop-opacity:1"/></linearGradient></defs><path d="m20 35-5 50q0 10 10 10h50q10 0 10-10l-5-50Z" fill="url(#a)" stroke="#4a5cd6" stroke-width="2"/><path d="M30 35q0-20 20-20t20 20" fill="none" stroke="#4a5cd6" stroke-width="3" stroke-linecap="round"/><circle cx="70" cy="25" r="4" fill="gold"/><circle cx="30" cy="70" r="3" fill="#fff" opacity=".7"/></svg> <svg data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108.43 100.53"><path d="M101.66 15.71c-4.16-.3-8.34-.35-12.51-.46-3.85-.1-7.69-.15-11.54-.21-9.14-.15-18.29-.32-27.44-.44-7.84-.11-15.68-.18-23.53-.21-.83 0-1.17-.3-1.33-1.01-.81-3.51-1.64-7.02-2.44-10.53-.31-1.33-1.42-2.36-2.68-2.41C18.6.37 17.01.27 15.42.23 11.37.13 7.31.06 3.25 0 1.27-.03 0 1.13 0 2.92 0 4.7 1.38 6.06 3.26 6.09c4.28.08 8.56.17 12.84.2.89 0 1.34.26 1.56 1.17 1.2 4.99 2.47 9.95 3.69 14.93 2.3 9.38 4.58 18.77 6.88 28.15 1.11 4.54 2.21 9.07 3.36 13.6.28 1.11.15 1.73-1.02 2.31-3.76 1.85-5.33 5.91-4.45 9.93.91 4.11 4.58 6.95 9.07 7.02h1.38c-2.97 1.75-4.68 4.13-4.95 7.42-.27 3.32 1.42 5.8 3.95 7.96-4.85.74-6.27.75-9.41 1.23.8.23 1.31.11 1.98.12 4.46.05 8.92.17 13.37.01 4.94-.17 8.86-5.16 7.57-10.63-.63-2.66-2.21-4.7-5.04-5.9h39.73c-2.87 1.74-4.53 4.14-4.85 7.36-.32 3.29 1.08 5.9 3.89 8.11-9.01.38-17.71.47-26.34 1.09l30.02.35c1.84-.07 3.73.03 5.49-.97 4.82-2.75 6.23-8.3 3.26-12.73-.84-1.26-2.17-2.19-3.21-3.2 1.3 0 2.83.03 4.35 0 1.66-.04 2.81-1.34 2.78-3.08-.02-1.56-1.25-2.77-2.82-2.79-6.68-.07-13.36-.18-20.04-.2-9.37-.04-18.74-.01-28.11-.02H35.44c-2.17 0-3.72-1.47-3.62-3.37.09-1.79 1.73-3.16 3.83-3.15 8.39.04 16.77.1 25.16.13 8.61.04 17.21.06 25.82.07.97 0 1.94-.09 2.9-.21 3.83-.52 6.67-3.16 7.69-6.89 1.84-6.75 3.76-13.47 5.65-20.21 1.36-4.84 2.79-9.66 4.08-14.52.59-2.2 1.13-4.45 1.32-6.7.29-3.53-2.89-6.7-6.6-6.96Zm-13.8 71.86c2.2-.07 4.11 1.95 4.1 4.15-.18 2.67-1.84 3.97-4.24 4.07-2.17.08-4.06-1.98-4.03-4.18.03-2.3 1.72-3.96 4.17-4.04m-47.43-.03c2.45-.06 4.19 1.8 4.15 4.03-.05 2.63-2.02 3.98-4.06 4.02-2.23.04-4.05-1.86-4.15-4.07-.1-2.22 2.05-4.07 4.06-3.98m30.45-67.01v12.33c-1.89 0-3.69.02-5.48 0-3.15-.05-6.3-.18-9.45-.18-.98 0-1.2-.35-1.27-1.24-.22-2.76-.55-5.5-.82-8.25-.09-.93-.15-1.86-.21-2.66zm-.14 17.64v12.64c-4.47 0-8.88.02-13.29-.04-.26 0-.71-.63-.75-1.01-.35-3.18-.62-6.37-.91-9.55v-.11c-.15-1.98-.15-1.95 1.83-1.94 4.35.02 8.69 0 13.13 0Zm-41.31-8.1c-.62-2.71-1.26-5.41-1.88-8.12-.15-.65-.27-1.32-.43-2.1 7.05.12 13.97.24 21.04.37.41 4.15.81 8.23 1.19 12.14H32.48c-.11 0-.22-.02-.32-.03-2.25-.14-2.24-.14-2.73-2.26m5.02 20.67c-1.01-4.24-2.02-8.49-3.03-12.7h18.64c.47 4.3.93 8.46 1.39 12.7H34.44Zm57.74 8.57c-.3 1.1-.54 2.23-.89 3.31-.51 1.58-1.87 2.54-3.47 2.54-16.08-.01-32.17-.04-48.25 0-1.26 0-1.71-.36-1.95-1.57-.44-2.27-1.1-4.5-1.65-6.75-.04-.17 0-.35 0-.67l56.99.39c-.29 1.03-.53 1.89-.77 2.76Zm4.75-16.54c-.7 2.51-1.41 5.02-2.17 7.51-.09.29-.56.65-.85.65q-8.385.06-16.77 0c-.29 0-.83-.42-.84-.64-.05-3.87-.04-7.75-.04-11.6h21.71c-.38 1.5-.69 2.8-1.05 4.08Zm5.38-19.31c-.83 2.95-1.7 5.89-2.49 8.85-.19.73-.47 1.01-1.23.99-6.45-.16-12.91-.28-19.36-.41-.94-.02-1.88 0-2.97 0 0-3.91.01-7.67 0-11.43 0-.76.45-.78 1-.77 2.83.08 5.65.17 8.48.22 4.93.09 9.86.15 14.79.22 1.49.02 2.18.94 1.78 2.34Z" style="fill:#477470;stroke-width:0"/></svg>

Before

Width:  |  Height:  |  Size: 588 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,5 +1,5 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection, isDevMode } from '@angular/core'; import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection, isDevMode } from '@angular/core';
import { PreloadAllModules, provideRouter, withPreloading, withInMemoryScrolling } from '@angular/router'; import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes'; import { routes } from './app.routes';
@@ -12,14 +12,14 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter( provideRouter(
routes, routes,
withPreloading(PreloadAllModules),
withInMemoryScrolling({ scrollPositionRestoration: 'top' }) withInMemoryScrolling({ scrollPositionRestoration: 'top' })
), ),
provideHttpClient( provideHttpClient(
withInterceptors([cacheInterceptor]) withInterceptors([cacheInterceptor])
), provideServiceWorker('ngsw-worker.js', { ),
provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(), enabled: !isDevMode(),
registrationStrategy: 'registerWhenStable:30000' registrationStrategy: 'registerWhenStable:30000'
}) })
] ]
} };

View File

@@ -1,21 +1,20 @@
@if (checkingServer()) { @if (checkingServer()) {
<div class="server-check-overlay"> <div class="server-check-overlay">
<div class="server-check-content">
<div class="spinner-large"></div> <div class="spinner-large"></div>
<h2>Проверка соединения с сервером...</h2> <p>{{ 'app.connecting' | translate }}</p>
</div>
</div> </div>
} @else if (!serverAvailable()) { } @else if (!serverAvailable()) {
<div class="server-error-overlay"> <div class="server-error-overlay">
<div class="server-error-content">
<div class="error-icon">⚠️</div> <div class="error-icon">⚠️</div>
<h1>Извините, возникла проблема</h1> <h2>{{ 'app.serverUnavailable' | translate }}</h2>
<p>Не удается подключиться к серверу. Пожалуйста, проверьте подключение к интернету или попробуйте позже.</p> <p>{{ 'app.serverError' | translate }}</p>
<button class="retry-btn" (click)="retryConnection()">Попробовать снова</button> <button class="retry-btn" (click)="retryConnection()">{{ 'app.retryConnection' | translate }}</button>
</div>
</div> </div>
} @else { } @else {
<app-header></app-header> <app-header></app-header>
@if (!isHomePage()) {
<app-back-button />
}
<main class="main-content"> <main class="main-content">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>

View File

@@ -1,5 +1,6 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { brandInfoRoutes, brandLegalRoutes } from './brands/brand-routes'; import { brandInfoRoutes, brandLegalRoutes } from './brands/brand-routes';
import { languageGuard } from './guards/language.guard';
// Core routes (same across all brands) // Core routes (same across all brands)
const coreRoutes: Routes = [ const coreRoutes: Routes = [
@@ -29,13 +30,18 @@ const coreRoutes: Routes = [
} }
]; ];
// Combine core routes with brand-specific routes // All routes sit under a :lang prefix (e.g. /ru/cart, /en/item/5)
export const routes: Routes = [ export const routes: Routes = [
{
path: ':lang',
canActivate: [languageGuard],
children: [
...coreRoutes, ...coreRoutes,
...brandInfoRoutes, ...brandInfoRoutes,
...brandLegalRoutes, ...brandLegalRoutes,
{ { path: '**', redirectTo: '' }
path: '**', ]
redirectTo: '' },
} // URLs without a language prefix → redirect to default language
{ path: '**', redirectTo: 'ru' }
]; ];

View File

@@ -7,81 +7,58 @@
.server-check-overlay, .server-check-overlay,
.server-error-overlay { .server-error-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f8f9fa;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 9999; min-height: 100vh;
}
.server-check-content,
.server-error-content {
text-align: center; text-align: center;
padding: 40px; padding: 2rem;
max-width: 500px; background: var(--surface-ground, #f8f9fa);
color: var(--text-color, #333);
} }
.spinner-large { .spinner-large {
width: 60px; width: 48px;
height: 60px; height: 48px;
border: 6px solid #f3f3f3; border: 4px solid var(--surface-border, #dee2e6);
border-top: 6px solid var(--primary-color); border-top-color: var(--primary-color, #007bff);
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 0.8s linear infinite;
margin: 0 auto 24px; margin-bottom: 1rem;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } to { transform: rotate(360deg); }
100% { transform: rotate(360deg); }
}
.server-check-content h2 {
color: #333;
font-size: 1.5rem;
margin: 0;
} }
.error-icon { .error-icon {
font-size: 5rem; font-size: 3rem;
margin-bottom: 20px; margin-bottom: 1rem;
} }
.server-error-content h1 { .server-error-overlay h2 {
font-size: 2rem; margin: 0 0 0.5rem;
color: #333; font-size: 1.25rem;
margin: 0 0 16px 0;
} }
.server-error-content p { .server-error-overlay p {
font-size: 1.1rem; margin: 0 0 1.5rem;
color: #333; opacity: 0.7;
line-height: 1.6; max-width: 300px;
margin: 0 0 32px 0;
} }
.retry-btn { .retry-btn {
padding: 14px 32px; padding: 0.75rem 2rem;
background: var(--primary-color);
color: white;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
font-size: 1.1rem; background: var(--primary-color, #007bff);
font-weight: 600; color: #fff;
font-size: 1rem;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: opacity 0.2s;
&:hover { &:hover {
background: var(--primary-hover); opacity: 0.85;
}
&:active {
transform: scale(0.98);
} }
} }

View File

@@ -1,70 +1,90 @@
import { Component, OnInit, OnDestroy, signal, ApplicationRef } from '@angular/core'; import { Component, OnInit, signal, ApplicationRef, inject, DestroyRef } from '@angular/core';
import { CommonModule } from '@angular/common'; import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
import { RouterOutlet } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { HeaderComponent } from './components/header/header.component'; import { HeaderComponent } from './components/header/header.component';
import { FooterComponent } from './components/footer/footer.component'; import { FooterComponent } from './components/footer/footer.component';
import { BackButtonComponent } from './components/back-button/back-button.component';
import { ApiService } from './services'; import { ApiService } from './services';
import { Subscription, interval, concat } from 'rxjs'; import { interval, concat } from 'rxjs';
import { first } from 'rxjs/operators'; import { filter, first } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { SwUpdate } from '@angular/service-worker'; import { SwUpdate } from '@angular/service-worker';
import { TranslatePipe } from './i18n/translate.pipe';
import { TranslateService } from './i18n/translate.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet, HeaderComponent, FooterComponent, CommonModule], imports: [RouterOutlet, HeaderComponent, FooterComponent, BackButtonComponent, TranslatePipe],
templateUrl: './app.html', templateUrl: './app.html',
styleUrl: './app.scss' styleUrl: './app.scss'
}) })
export class App implements OnInit, OnDestroy { export class App implements OnInit {
protected title = environment.brandName; protected title = environment.brandName;
serverAvailable = signal(true); isHomePage = signal(true);
checkingServer = signal(true); checkingServer = signal(true);
private pingSubscription?: Subscription; serverAvailable = signal(false);
private updateSubscription?: Subscription;
constructor( private destroyRef = inject(DestroyRef);
private apiService: ApiService, private apiService = inject(ApiService);
private titleService: Title, private titleService = inject(Title);
private swUpdate: SwUpdate, private swUpdate = inject(SwUpdate);
private appRef: ApplicationRef private appRef = inject(ApplicationRef);
) {} private router = inject(Router);
private i18n = inject(TranslateService);
ngOnInit(): void { ngOnInit(): void {
// Устанавливаем заголовок страницы в зависимости от бренда this.titleService.setTitle(`${environment.brandFullName} - ${this.i18n.t('app.pageTitle')}`);
this.titleService.setTitle(`${environment.brandFullName} - Маркетплейс товаров и услуг`);
this.checkServerHealth(); this.checkServerHealth();
this.setupAutoUpdates(); this.setupAutoUpdates();
// Track route changes to show/hide back button
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((event) => {
const navEnd = event as NavigationEnd;
const url = navEnd.urlAfterRedirects || navEnd.url;
// Home pages: /ru, /en, /hy (with or without trailing slash)
this.isHomePage.set(/^\/[a-z]{2}\/?$/.test(url) || url === '/' || url === '');
});
} }
checkServerHealth(): void { private checkServerHealth(): void {
this.pingSubscription = this.apiService.ping().subscribe({ this.checkingServer.set(true);
next: (response) => { this.apiService.ping()
// Server is available .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
this.serverAvailable.set(true); this.serverAvailable.set(true);
this.checkingServer.set(false); this.checkingServer.set(false);
}, },
error: (err) => { error: () => {
console.error('Server health check failed:', err); this.serverAvailable.set(false);
// Allow app to continue even if server is unreachable
this.serverAvailable.set(true);
this.checkingServer.set(false); this.checkingServer.set(false);
} }
}); });
} }
setupAutoUpdates(): void { retryConnection(): void {
this.checkServerHealth();
}
private setupAutoUpdates(): void {
if (!this.swUpdate.isEnabled) { if (!this.swUpdate.isEnabled) {
return; return;
} }
// Check for updates every 6 hours
const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true)); const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
const every6Hours$ = interval(6 * 60 * 60 * 1000); const every6Hours$ = interval(6 * 60 * 60 * 1000);
const checkInterval$ = concat(appIsStable$, every6Hours$); const checkInterval$ = concat(appIsStable$, every6Hours$);
this.updateSubscription = checkInterval$.subscribe(async () => { checkInterval$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(async () => {
try { try {
await this.swUpdate.checkForUpdate(); await this.swUpdate.checkForUpdate();
} catch (err) { } catch (err) {
@@ -72,22 +92,12 @@ export class App implements OnInit, OnDestroy {
} }
}); });
// Silently activate updates when ready this.swUpdate.versionUpdates
this.swUpdate.versionUpdates.subscribe(event => { .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(event => {
if (event.type === 'VERSION_READY') { if (event.type === 'VERSION_READY') {
// Update will activate on next navigation/reload automatically
console.log('New app version ready'); console.log('New app version ready');
} }
}); });
} }
ngOnDestroy(): void {
this.pingSubscription?.unsubscribe();
this.updateSubscription?.unsubscribe();
}
retryConnection(): void {
this.checkingServer.set(true);
this.checkServerHealth();
}
} }

View File

@@ -1,74 +0,0 @@
<div class="legal-page">
<div class="legal-container">
<h1>О компании ООО «ИНТ ФИН ЛОГИСТИК»</h1>
<section class="legal-section">
<h2>О нас</h2>
<p>Компания DexarMarket действительно представляет собой быстроразвивающийся маркетплейс, активно функционирующий в области торговли различными товарами и услугами. Регистрация юридического лица ООО "ИНТ ФИН ЛОГИСТИК", осуществленная согласно законодательству Армении, подчеркивает международную направленность бизнеса, поскольку компания также успешно работает на российском рынке, имея необходимые реквизиты для легальной деятельности в РФ.</p>
<p>Начало своей деятельности DEXARMARKET положил именно в Армении, однако расширение произошло стремительно. Уже летом 2025 года площадка вышла на российский рынок, показывая значительный рост популярности среди партнеров и покупателей из разных регионов мира, включая Россию, Объединённые Арабские Эмираты, Турцию, Китай, Армению, Казахстан, Кыргызстан и другие государства.</p>
<p>Основная цель компании заключается в предоставлении качественного сервиса своим партнерам и покупателям, обеспечивая комфорт и надежность сделок, расширяя ассортимент товаров и услуг, а также поддерживая высокие стандарты обслуживания клиентов.</p>
<p>Таким образом, DEXARMARKET является ярким примером успешного международного проекта, демонстрирующего успешное развитие и интеграцию на глобальном уровне.</p>
</section>
<section class="legal-section">
<h2>Наша миссия</h2>
<p>Мы стремимся создать уникальную экосистему, обеспечивающую максимальный комфорт и выгоду нашим партнёрам при размещении товаров на платформе DEXARMARKET. Мы предлагаем широкий спектр дополнительных услуг и сервисов, помогающих оптимизировать бизнес-процессы наших продавцов.</p>
<p>Для покупателей мы предоставляем удобный доступ к разнообразному ассортименту качественных товаров по привлекательным ценам непосредственно от производителей и поставщиков. Это позволяет клиентам экономить время и средства, выбирая лучшие предложения на рынке.</p>
<h3>Нашими основными приоритетами являются:</h3>
<ul>
<li>Создание прозрачной и эффективной системы взаимодействия между продавцами и покупателями.</li>
<li>Предоставление инновационных решений для повышения конкурентоспособности и увеличения продаж наших партнёров.</li>
<li>Обеспечение высокого уровня клиентского сервиса и поддержки пользователей на всех этапах сотрудничества.</li>
</ul>
<p>Мы постоянно работаем над улучшением функционала нашей платформы, внедряя новые технологии и инструменты, чтобы сделать процесс покупки и продажи ещё более удобным и выгодным для всех участников рынка.</p>
</section>
<section class="legal-section">
<h2>История компании DEXARMARKET</h2>
<p>Компания DEXARMARKET возникла благодаря усилиям команды профессионалов, объединённых общей целью и опытом в ключевых областях: информационно-коммуникационных технологиях (IT), экономике и торговой отрасли. Основатели понимали важность интеграции передовых технологических решений и глубокого понимания рыночных процессов, что позволило сформировать эффективную концепцию развития маркетплейса.</p>
<h3>Основные этапы становления:</h3>
<h4>Зарождение идеи</h4>
<p>Команда, обладающая глубокими познаниями в цифровой трансформации бизнеса и экономических процессах, объединилась вокруг амбициозной цели: создание площадки, способствующей развитию предпринимательства и коммерции на международном уровне.</p>
<h4>Разработка концепции</h4>
<p>Осознавая необходимость предоставления удобных инструментов для эффективного управления бизнесом онлайн, команда приступила к разработке оригинальной модели маркетплейса, основанной на последних достижениях в области электронной коммерции и аналитики больших данных.</p>
<h4>Запуск платформы</h4>
<p>Используя инновационные решения в информационной инфраструктуре и надёжные механизмы защиты данных, команда создала стабильную цифровую среду, привлекательную для предпринимателей и конечных потребителей.</p>
<h4>Выход на международный уровень</h4>
<p>Получив признание на национальном рынке, DEXARMARKET начал своё активное продвижение на международные рынки, постепенно охватывая разные регионы и страны, создавая условия для роста малого и среднего бизнеса, а также крупных торговых компаний.</p>
<p><strong>Сегодня DEXARMARKET продолжает развиваться, совершенствуя свои технологические возможности и предлагая пользователям всё больше преимуществ и возможностей для успешной коммерческой деятельности.</strong></p>
</section>
<section class="legal-section">
<h2>Контактная информация</h2>
<p><strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></p>
<p><strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a></p>
<p><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></p>
<p><strong>Часы работы:</strong><br>Техподдержка: 24/7<br>Ответы на вопросы: 10:00 - 19:00 (МСК)</p>
</section>
<section class="legal-section">
<h2>Реквизиты организации</h2>
<p><strong>Полное наименование организации:</strong><br>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФИН ЛОГИСТИК»</p>
<p><strong>Директор:</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Юридический адрес:</strong><br>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
<p><strong>Офис в Армении:</strong><br>0033, Ереван, улица Братьев Орбели, 47</p>
<p><strong>Офис в России:</strong><br>121059, Москва, наб. Тараса Шевченко, 3к2</p>
<p><strong>Основные реквизиты:</strong><br>ИНН (РФ): 9909697628<br>ИНН (Армения): 03033502<br>КПП: 770287001<br>ОГРН: 85.110.1408711</p>
<p><strong>Банковские реквизиты:</strong><br>Банк: АО "Райффайзенбанк"<br>Расчетный счет: 40807810500000002376<br>Корр. счет: 30101810200000000700<br>БИК: 044525700</p>
<p><strong>Контактная информация:</strong><br>Телефон (Россия): +7 (926) 459-31-57<br>Телефон (Армения): +374 94 86 18 16<br>Email: info@dexarmarket.ru<br>Сайт: www.dexarmarket.ru</p>
</section>
</div>
</div>

View File

@@ -1,46 +0,0 @@
<div class="legal-page">
<div class="legal-container">
<h1>Контакты</h1>
<section class="legal-section">
<h2>Контактная информация</h2>
<p>Свяжитесь с нами по любым вопросам, связанным с работой маркетплейса, оформлением заказов или возвратом товаров.</p>
</section>
<section class="legal-section">
<h2>Реквизиты организации</h2>
<p><strong>Наименование:</strong> ООО «ИНТ ФИН ЛОГИСТИК»</p>
<p><strong>ИНН:</strong> 9909697628</p>
</section>
<section class="legal-section">
<h2>Телефоны</h2>
<p><strong>Россия:</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></p>
<p><strong>Армения:</strong> <a href="tel:+37494861816">+374 94 86 18 16</a></p>
</section>
<section class="legal-section">
<h2>Email</h2>
<p>По всем вопросам пишите на: <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></p>
<p>Мы стараемся отвечать на все письма в течение 24 часов в рабочие дни.</p>
</section>
<section class="legal-section">
<h2>Часы работы</h2>
<p><strong>Техническая поддержка:</strong> Круглосуточно (24/7)</p>
<p><strong>Ответы на вопросы:</strong> Понедельник - Пятница: 10:00 - 19:00 (МСК)</p>
<p>Суббота - Воскресенье: выходной</p>
</section>
<section class="legal-section">
<h2>Адреса офисов</h2>
<p><strong>Офис в Армении:</strong> 0033, Ереван, улица Братьев Орбели, 47</p>
<p><strong>Офис в России:</strong> 121059, Москва, наб. Тараса Шевченко, 3к2</p>
</section>
<section class="legal-section">
<h2>Техническая поддержка</h2>
<p>При возникновении технических проблем с работой сайта или вопросов по оформлению заказа обращайтесь на <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a> с подробным описанием проблемы.</p>
</section>
</div>
</div>

View File

@@ -1,60 +0,0 @@
<div class="legal-page">
<div class="legal-container">
<h1>Доставка</h1>
<section class="legal-section">
<h2>1. Способы доставки</h2>
<p>1.1. <strong>Цифровые товары:</strong> Платформа DexarMarket осуществляет доставку цифровых товаров (лицензионные ключи, электронные сертификаты, цифровой контент и т.д.) на указанную Покупателем электронную почту или в личный кабинет на платформе незамедлительно после подтверждения оплаты.</p>
<p>1.2. <strong>Материальные товары:</strong> Продавец самостоятельно осуществляет доставку материальных Товаров с привлечением транспортных компаний и почтовых операторов, включая, но не ограничиваясь: <strong>АО «СДЭК»</strong>, <strong>ФГУП «Почта России»</strong>, <strong>Boxberry</strong>, <strong>DPD</strong>, <strong>Яндекс.Доставка</strong> и иных перевозчиков по согласованию с Покупателем.</p>
<p>1.3. Способы доставки материальных товаров, доступные для конкретного Заказа, определяются Продавцом на момент оформления Заказа и могут включать:</p>
<ul>
<li>курьерскую доставку (АО «СДЭК», DPD, Яндекс.Доставка, иные курьерские службы);</li>
<li>доставку в пункт выдачи (Boxberry, СДЭК, Почта России);</li>
<li>почтовую отправку (ФГУП «Почта России»);</li>
<li>самовывоз (если применимо для данного Товара).</li>
</ul>
<p>1.4. Выбор конкретного способа доставки осуществляется Покупателем в процессе оформления Заказа в пределах доступных вариантов, предложенных Продавцом.</p>
</section>
<section class="legal-section">
<h2>2. Стоимость доставки</h2>
<p>2.1. <strong>Цифровые товары:</strong> Доставка цифровых товаров осуществляется бесплатно.</p>
<p>2.2. <strong>Материальные товары:</strong> Стоимость доставки материальных товаров определяется Продавцом в зависимости от выбранного способа доставки, веса и габаритов отправления, адреса доставки и действующих тарифов перевозчика на момент оформления Заказа.</p>
<p>2.3. Итоговая стоимость доставки материальных товаров отображается Покупателю до подтверждения Заказа и включается в общую сумму к оплате.</p>
<p>2.4. В случае проведения акций и специальных предложений доставка может быть осуществлена бесплатно при выполнении условий, указанных в описании акции.</p>
</section>
<section class="legal-section">
<h2>3. Сроки доставки</h2>
<p>3.1. <strong>Цифровые товары:</strong> Доставка цифровых товаров производится мгновенно (в течение нескольких минут) после подтверждения оплаты. В отдельных случаях срок может составлять до 24 часов.</p>
<p>3.2. <strong>Материальные товары:</strong> Ориентировочные сроки доставки материальных товаров по территории Российской Федерации составляют от 1 (одного) до 14 (четырнадцати) рабочих дней с момента передачи отправления перевозчику Продавцом.</p>
<p>3.3. Указанные сроки являются приблизительными и могут изменяться в зависимости от региона доставки, работы перевозчика, погодных условий, выходных и праздничных дней.</p>
<p>3.4. Точный срок доставки материальных товаров рассчитывается автоматически при оформлении Заказа на основании данных выбранного перевозчика и доводится до сведения Покупателя.</p>
<p>3.5. Продавец не несёт ответственности за нарушение сроков доставки, вызванное действиями или бездействием перевозчика, обстоятельствами непреодолимой силы либо иными независящими от Продавца причинами.</p>
</section>
<section class="legal-section">
<h2>4. Условия передачи и приёмки Товара</h2>
<p>4.1. Товар передаётся Покупателю или указанному им лицу перевозчиком в соответствии с условиями выбранного способа доставки.</p>
<p>4.2. Риск случайной гибели или повреждения Товара переходит к Покупателю с момента фактической передачи Товара Покупателю или уполномоченному им лицу.</p>
<p>4.3. При получении Товара Покупатель обязан:</p>
<ul>
<li>проверить целостность упаковки и соответствие наименования и количества Товара данным, указанным в сопроводительных документах;</li>
<li>в случае обнаружения повреждений упаковки, недостачи или несоответствия Товара — составить акт в присутствии представителя перевозчика;</li>
<li>при отсутствии претензий — расписаться в документах перевозчика о получении Товара в надлежащем состоянии.</li>
</ul>
<p>4.4. Отправка Товара осуществляется Продавцом после подтверждения Заказа и получения оплаты (полной или частичной) в соответствии с условиями конкретного Заказа.</p>
<p>4.5. Продавец не несёт ответственности за утрату, повреждение или задержку Товара в период транспортировки, если указанные обстоятельства вызваны действиями или бездействием перевозчика. В таких случаях Покупатель вправе предъявить претензии непосредственно перевозчику в установленном законодательством порядке, а Продавец обязуется оказать содействие в урегулировании спора в разумных пределах.</p>
</section>
<section class="legal-section">
<h2>5. Контактная информация</h2>
<p>По вопросам доставки обращайтесь:</p>
<ul>
<li><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></li>
<li><strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></li>
<li><strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a></li>
</ul>
</section>
</div>
</div>

View File

@@ -1,244 +0,0 @@
<div class="legal-page">
<div class="legal-container">
<h1>Часто задаваемые вопросы (FAQ) 📌</h1>
<section class="legal-section">
<h2>Общие вопросы</h2>
<div class="faq-item">
<h3>Что такое DexarMarket?</h3>
<p>DexarMarket — это онлайн-площадка, где независимые продавцы размещают свои товары и услуги. Наша задача — обеспечить удобную и безопасную среду для совершения покупок, мы сами не производим и не продаем товары.</p>
</div>
<div class="faq-item">
<h3>Как зарегистрироваться на платформе?</h3>
<p>Зарегистрироваться легко: откройте наше приложение внутри Telegram. Ваш профиль будет создан автоматически сразу же после входа.</p>
</div>
<div class="faq-item">
<h3>Безопасно ли покупать на сайте?</h3>
<p>Абсолютно да! Все транзакции защищены современными технологиями шифрования (PCI DSS и 3D Secure). Ваши банковские данные надежно хранятся отдельно от нашей системы.</p>
</div>
</section>
<section class="legal-section">
<h2>Оформление заказа</h2>
<div class="faq-item">
<h3>Как правильно оформить покупку?</h3>
<p>
1⃣ Найдите понравившийся товар и добавьте его в корзину.<br>
2⃣ Открыв корзину, кликните «Оформить заказ».<br>
3⃣ Укажите адрес доставки и контактную информацию.<br>
4⃣ Выберите удобный способ оплаты.<br>
5⃣ Ознакомьтесь и подтвердите публичную оферту.<br>
6⃣ Завершите оплату вашего заказа.
</p>
</div>
<div class="faq-item">
<h3>Можно ли внести изменения в заказ после завершения покупки?</h3>
<p>Если продавец ещё не отправил товар, напишите нам в поддержку — мы поможем скорректировать заказ. Но после отправки товара изменение невозможно.</p>
</div>
<div class="faq-item">
<h3>Как отказаться от заказа?</h3>
<p>Свяжитесь с нами или непосредственно с продавцом через чаты заказов. Пока посылка не отправлена, отказ возможен с полным возвратом денег.</p>
</div>
</section>
<section class="legal-section">
<h2>Оплата 💳</h2>
<div class="faq-item">
<h3>Какие способы оплаты доступны?</h3>
<p>Принимаются:</p>
<ul>
<li>Банковские карты Visa, MasterCard, МИР,</li>
<li>Система быстрых платежей (СБП),</li>
<li>Электронные кошельки ЮMoney, QIWI,</li>
<li>Наличные при получении (если предусмотрено продавцом).</li>
</ul>
</div>
<div class="faq-item">
<h3>Когда спишутся средства с моей карты?</h3>
<p>Операция проходит мгновенно, но деньги временно замораживаются до момента получения вами товара. Как только вы подтверждаете получение, средства отправляются продавцу.</p>
</div>
<div class="faq-item">
<h3>Почему мой платёж мог быть отклонён?</h3>
<p>Возможные причины отказа:</p>
<ul>
<li>Недостаточно средств на счёте,</li>
<li>Превышение лимита операций по карте,</li>
<li>Карта заблокирована банком,</li>
<li>Технические сбои.</li>
</ul>
<p>Рекомендуем обратиться в банк для выяснения деталей.</p>
</div>
<div class="faq-item">
<h3>Получу ли я чек об оплате?</h3>
<p>Да, электронная квитанция придёт на указанный вами e-mail сразу после успешного платежа согласно закону № 54-ФЗ.</p>
</div>
</section>
<section class="legal-section">
<h2>Доставка 🚚</h2>
<div class="faq-item">
<h3>Через какие службы осуществляется доставка?</h3>
<p><strong>Цифровые товары:</strong> Платформа DexarMarket автоматически отправляет цифровые товары (лицензии, ключи, сертификаты) на вашу электронную почту или в личный кабинет мгновенно после оплаты.</p>
<p><strong>Материальные товары:</strong> Продавец самостоятельно работает с ведущими службами доставки:</p>
<ul>
<li>СДЭК,</li>
<li>Почта России,</li>
<li>Boxberry,</li>
<li>DPD,</li>
<li>Яндекс.Доставка.</li>
</ul>
<p>Способ доставки материальных товаров выбирается исходя из предпочтений продавца и места назначения.</p>
</div>
<div class="faq-item">
<h3>Сколько стоит доставка моего заказа?</h3>
<p><strong>Цифровые товары:</strong> Доставка бесплатная — товар придёт на email мгновенно.</p>
<p><strong>Материальные товары:</strong> Цена определяется продавцом и зависит от веса, габаритов товара, выбранного способа и региона доставки. Окончательная стоимость видна при оформлении заказа.</p>
</div>
<div class="faq-item">
<h3>Какой срок доставки моего заказа?</h3>
<p><strong>Цифровые товары:</strong> Мгновенная доставка на email (в течение нескольких минут после оплаты).</p>
<p><strong>Материальные товары</strong> (примерные сроки):</p>
<ul>
<li>СДЭК: 27 рабочих дней,</li>
<li>Почта России: 514 рабочих дней,</li>
<li>Boxberry: 25 рабочих дней,</li>
<li>DPD: 13 рабочих дня,</li>
<li>Яндекс.Доставка: в тот же день (при наличии возможности в вашем городе).</li>
</ul>
</div>
<div class="faq-item">
<h3>Как отслеживать мою посылку?</h3>
<p>Вы получите трек-код на свою электронную почту и сможете увидеть статус заказа в личном кабинете. Отслеживать посылку можно на официальном сайте выбранной курьерской службы.</p>
</div>
<div class="faq-item">
<h3>Что делать, если товар пришёл повреждённым? ⛑</h3>
<p>Осмотрите товар прямо при курьере. Если обнаружились дефекты — оформляйте акт отказа от приёмки и незамедлительно сообщайте продавцу и службе поддержки.</p>
</div>
</section>
<section class="legal-section">
<h2>Возврат и обмен ✅</h2>
<div class="faq-item">
<h3>Можно ли вернуть товар?</h3>
<p>Да, закон позволяет вернуть качественный товар в течение 7 дней после получения. Бракованные изделия возвращаются по особым правилам.</p>
</div>
<div class="faq-item">
<h3>Какие товары вернуть нельзя?</h3>
<p>Нельзя вернуть товары, перечисленные в Постановлении Правительства РФ №2463: медикаменты, косметику, бельё, активированные цифровые продукты и индивидуальные заказы. Подробности смотрите в разделе <a routerLink="/return-policy">«Политика возврата»</a>.</p>
</div>
<div class="faq-item">
<h3>Как вернуть деньги?</h3>
<p>
1. Сообщите продавцу о намерении вернуть товар.<br>
2. Верните товар в оригинальной упаковке.<br>
3. Продавец проверит состояние товара и вернёт средства таким же способом, каким была произведена покупка (до 30 дней ожидания возврата).
</p>
</div>
<div class="faq-item">
<h3>Кто оплатит обратный путь?</h3>
<p>Покупатель оплачивает обратную доставку качественного товара. Если обнаружен брак — расходы несёт продавец.</p>
</div>
</section>
<section class="legal-section">
<h2>Гарантия 🔧</h2>
<div class="faq-item">
<h3>Есть ли гарантия на товары?</h3>
<p>Большинство наших товаров имеют официальную гарантию производителя. Срок варьируется от 12 до 36 месяцев и указывается в карточке каждого продукта.</p>
</div>
<div class="faq-item">
<h3>Как воспользоваться гарантией?</h3>
<p>Сообщите продавцу о неисправности, приложив фотографии дефектов и гарантийный талон. Продавец займётся ремонтом или заменой товара.</p>
</div>
<div class="faq-item">
<h3>Что не покрыто гарантией?</h3>
<p>Механические повреждения, последствия неправильной эксплуатации, самостоятельный ремонт, влияние влаги (если нет защиты), естественный износ — всё это не является страховым случаем.</p>
</div>
</section>
<section class="legal-section">
<h2>Безопасность и конфиденциальность 🔐</h2>
<div class="faq-item">
<h3>Как вы защищаете мои личные данные?</h3>
<p>Используем современные методы шифрования SSL/TLS, не храним реквизиты карт, выполняем требования закона №152-ФЗ о защите персональной информации. Подробности — в <a routerLink="/privacy-policy">Политике конфиденциальности</a>.</p>
</div>
<div class="faq-item">
<h3>Кому предоставляются мои данные?</h3>
<p>Только вашему продавцу для обработки заказа и службам доставки. Данные не используются третьими лицами для маркетинга без вашего разрешения.</p>
</div>
<div class="faq-item">
<h3>Как удалить свой аккаунт?</h3>
<p>Напишите нам в службу поддержки запрос на удаление учётной записи. Аккаунт будет ликвидирован вместе с личной информацией в течение месяца.</p>
</div>
</section>
<section class="legal-section">
<h2>Информация для продавцов 📋</h2>
<div class="faq-item">
<h3>Как начать продавать на площадке?</h3>
<p>Чтобы присоединиться к нашим продавцам, обратитесь в службу поддержки по электронной почте <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a>.</p>
</div>
<div class="faq-item">
<h3>Какова комиссия площадки?</h3>
<p>Комиссия зависит от типа товара и объёма продаж. Узнать точные условия можно при регистрации.</p>
</div>
<div class="faq-item">
<h3>Когда поступят деньги за продажу?</h3>
<p>Продавец получает средства после подтверждения клиентом получения товара либо спустя две недели с момента доставки, если клиент не указал проблем с покупкой.</p>
</div>
</section>
<section class="legal-section">
<h2>Служба поддержки 💬</h2>
<div class="faq-item">
<h3>Как связаться с поддержкой?</h3>
<p>
✉️ <strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a><br>
📞 <strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a><br>
📞 <strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a><br>
🏢 <strong>Время работы офиса:</strong> 10:00—19:00 (МСК)<br>
❄️ <strong>Техподдержка доступна круглосуточно.</strong>
</p>
</div>
<div class="faq-item">
<h3>Сколько ждать ответа?</h3>
<p>Во время рабочего дня ответ поступает в течение двух часов. В праздники и выходные возможны задержки до суток.</p>
</div>
</section>
<section class="legal-section">
<h2>Нужна помощь?</h2>
<p>Если возникнут дополнительные вопросы, обращайтесь на <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> — мы оперативно решим любые ваши вопросы!</p>
</section>
</div>
</div>

View File

@@ -1,158 +0,0 @@
<div class="legal-page">
<div class="legal-container">
<h1>Гарантия 🔨</h1>
<section class="legal-section">
<h2>1. Основные положения о гарантии</h2>
<p>Настоящий раздел устанавливает порядок предоставления гарантийных услуг на товары, купленные на маркетплейсе DexarMarket.</p>
<ul>
<li>Обязательства по гарантии исполняет сам Продавец товара, в строгом соответствии с российским законодательством.</li>
<li>Платформа DexarMarket выступает лишь информационным посредником и не принимает участие в исполнении гарантийных условий.</li>
<li>Гарантия действует исключительно на заводские дефекты и недостатки, возникшие не по вине покупателя.</li>
</ul>
</section>
<section class="legal-section">
<h2>2. Срок гарантии 🏷</h2>
<p>Срок гарантии устанавливается Продавцом или производителем товара и публикуется:</p>
<ul>
<li>На страницах товаров нашего сайта.</li>
<li>В гарантийном талоне, вложенном в упаковку.</li>
<li>В сопроводительной документации товара.</li>
</ul>
<p><strong>Типичные сроки гарантии по категориям товаров:</strong></p>
<ul>
<li><strong>Электроника и бытовая техника:</strong> от 12 до 24 месяцев.</li>
<li><strong>Компьютерная техника и комплектующие:</strong> от 12 до 36 месяцев.</li>
<li><strong>Одежда и обувь:</strong> от 30 дней до полугода (зависит от сезонности).</li>
<li><strong>Мебель:</strong> от 12 до 18 месяцев.</li>
<li><strong>Цифровая продукция:</strong> поддержка определяется самим Продавцом.</li>
</ul>
<p>Начало срока гарантии отсчитывается с момента передачи товара покупателю.</p>
<p>Замена товара продлевает гарантийный срок заново с момента выдачи замены.</p>
<p>Если срок гарантии не обозначен Продавцом, покупатель имеет право предъявлять претензии в течение 2-х лет с момента приобретения товара (согласно ст. 19 Закона РФ «О защите прав потребителей»).</p>
</section>
<section class="legal-section">
<h2>3. Условия предоставления гарантии 📝</h2>
<p>Гарантия действительна при выполнении следующих требований:</p>
<ul>
<li>Использование товара строго по инструкции.</li>
<li>Отсутствие самостоятельной разборки, ремонта или модификации устройства.</li>
<li>Нет механических повреждений внешнего корпуса и внутренних элементов.</li>
<li>Сохранены оригинальные пломбы и серийные номера (если предусмотрены).</li>
<li>Устройство не подвергалось влиянию высоких температур, влажности или химикатов.</li>
<li>Имеются гарантийный талон и подтверждение покупки (чек, квитанция).</li>
</ul>
<p><strong>Документы для обращения по гарантии:</strong></p>
<ul>
<li>Сам товар с полной комплектацией.</li>
<li>Гарантийный талон (если прилагался).</li>
<li>Документ, подтверждающий приобретение (чек, кассовый ордер).</li>
<li>Удостоверение личности владельца товара (например, паспорт).</li>
</ul>
</section>
<section class="legal-section">
<h2>4. Гарантийный ремонт и замена 🛠</h2>
<h3>Права покупателя при выявлении брака:</h3>
<p>Если недостаток найден в пределах гарантийного периода, вы имеете право:</p>
<ul>
<li>Бесплатно устранить неисправность.</li>
<li>Получить аналогичный товар взамен испорченного.</li>
<li>Потребовать замену на другой товар с перерасчетом стоимости.</li>
<li>Снизить цену товара пропорционально дефекту.</li>
<li>Вернуть полную сумму за товар.</li>
</ul>
<h3>Сроки ремонта:</h3>
<p>Ремонт выполняется быстро, но максимальный срок составляет 45 дней (статья 20 Закона РФ «О защите прав потребителей»). Если срок нарушен, вы можете попросить заменить товар или вернуть деньги.</p>
<h3>Временная замена товара:</h3>
<p>Если срок ремонта превышает неделю, продавец обязан предоставить временный заменитель для технически сложных товаров.</p>
<h3>Доставка для ремонта:</h3>
<p>Расходы на транспортировку товара в сервисный центр и обратно берет на себя продавец или специализированный сервисный центр.</p>
</section>
<section class="legal-section">
<h2>5. Случаи, не подлежащие гарантии 🔍</h2>
<p>Гарантия не работает, если выявлены следующие обстоятельства:</p>
<ul>
<li>Механическое повреждение (удары, падение, трещины, царапины);</li>
<li>Нарушения правил эксплуатации (неправильное подключение, превышение нагрузки, нестандартное применение);</li>
<li>Повреждения из-за внешних воздействий (жидкость, грязь, высокие температуры, сырость);</li>
<li>Самостоятельный ремонт (разборка, модернизация, замена комплектующих);</li>
<li>Действие форс-мажорных обстоятельств (пожар, затопление, кража, погодные катаклизмы);</li>
<li>Естественное старение материалов (потеря цвета, блеск, незначительный износ);</li>
<li>Незаконное нарушение заводских пломб или уничтожение серийных номеров.</li>
</ul>
<p><strong>Также не относятся к гарантийному случаю:</strong></p>
<ul>
<li>Косметические изъяны, не влияющие на работу (поверхностные царапинки, небольшие пятна);</li>
<li>Изменения внешнего вида вследствие обычной эксплуатации;</li>
<li>Программные неполадки, вызванные установками постороннего программного обеспечения;</li>
<li>Проблемы совместимости с устройствами или ПО других производителей.</li>
</ul>
<p>Отдельно оговорено ограничение гарантии на расходники (батарейки, лампочки, фильтры), указанные в описании товара.</p>
</section>
<section class="legal-section">
<h2>6. Процедура подачи заявки на гарантийное обслуживание 🗒</h2>
<p>Чтобы воспользоваться гарантийным сервисом, выполните следующие шаги:</p>
<ol>
<li>Связаться с продавцом через контактные данные, указанные в вашем заказе.</li>
<li>Объяснить суть проблемы и приложить фотоматериалы или видеозапись (если необходимо).</li>
<li>Получить от продавца инструкцию по обращению в сервисный центр или адрес, куда отправить товар.</li>
<li>Доставить товар в сервис с документами, подтверждающими покупку (гарантийный талон, чек).</li>
<li>Получить акт приёма товара с указанием срока ремонтных работ.</li>
<li>Забрать восстановленный товар после оповещения о завершении ремонта.</li>
</ol>
<h3>Правила отправки товара почтой или курьером:</h3>
<ul>
<li>Надежно упакуйте устройство, предотвращая возможные повреждения при перевозке.</li>
<li>Положите копии документов о покупке и подробное описание проблемы внутрь упаковки.</li>
<li>Оформите почтовое отправление с оценочной стоимостью.</li>
<li>Обязательно сохраняйте номер трека для контроля местонахождения груза.</li>
</ul>
<p>Если возникли трудности, покупатели также могут обращаться в нашу службу поддержки через email: <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a>.</p>
</section>
<section class="legal-section">
<h2>7. Дополнительные права покупателя 🎯</h2>
<p>Если товар имеет серьёзный недостаток, вы вправе:</p>
<ul>
<li>Требовать полного возврата денег.</li>
<li>Просить замену на товар другой модели с соответствующим перерасчётом стоимости.</li>
</ul>
<p><strong>Серьёзный недостаток</strong> — это ситуация, когда:</p>
<ul>
<li>Невозможно исправить поломку.</li>
<li>Устранение поломки требует больших затрат или долгого времени.</li>
<li>Недостаток появляется снова после ремонта.</li>
<li>Одна и та же проблема возникает многократно.</li>
</ul>
<p>Кроме того, вы можете взыскать убытки, понесённые из-за продажи некачественного товара.</p>
</section>
<section class="legal-section">
<h2>8. Контактная информация 📞</h2>
<p>По любым вопросам гарантийного обслуживания обращайтесь сначала к продавцу (см. страницу товара или ваше уведомление о доставке).</p>
<p><strong>Если возникла необходимость решения спора:</strong></p>
<p>Отправьте письмо на email Маркетплейса: <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> с темой: «Гарантийный вопрос — Заказ №[номер заказа]».</p>
<p>В случае отказа продавца принять претензию, вы имеете право инициировать независимую экспертизу качества товара и подать иск в судебные органы.</p>
</section>
</div>
</div>

View File

@@ -1,102 +0,0 @@
<div class="legal-page">
<div class="legal-container">
<h1>Реквизиты организации</h1>
<section class="legal-section">
<h2>Полное наименование организации</h2>
<p>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФИН ЛОГИСТИК»</p>
<p><strong>Сокращенное наименование:</strong> ООО «ИНТ ФИН ЛОГИСТИК»</p>
</section>
<section class="legal-section">
<h2>Юридический адрес</h2>
<p>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
</section>
<section class="legal-section">
<h2>Фактический адрес</h2>
<p><strong>Офис в Армении:</strong> 0033, Ереван, улица Братьев Орбели, 47</p>
<p><strong>Офис в России:</strong> 121059, Москва, наб. Тараса Шевченко, 3к2</p>
</section>
<section class="legal-section">
<h2>Основные реквизиты</h2>
<div class="details-grid">
<div class="detail-item">
<strong>ИНН (RUS):</strong>
<span>9909697628</span>
</div>
<div class="detail-item">
<strong>ИНН (ARM):</strong>
<span>03033502</span>
</div>
<div class="detail-item">
<strong>КПП:</strong>
<span>770287001</span>
</div>
<div class="detail-item">
<strong>ОГРН:</strong>
<span>85.110.1408711</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Банковские реквизиты</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Банк:</strong>
<span>АО "Райффайзенбанк"</span>
</div>
<div class="detail-item">
<strong>Расчетный счет:</strong>
<span>40807810500000002376</span>
</div>
<div class="detail-item">
<strong>Корр. счет:</strong>
<span>30101810200000000700</span>
</div>
<div class="detail-item">
<strong>БИК:</strong>
<span>044525700</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Контактная информация</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Телефон (RUS):</strong>
<span><a href="tel:+79264593157">+7 (926) 459-31-57</a></span>
</div>
<div class="detail-item">
<strong>Телефон (ARM):</strong>
<span><a href="tel:+37494861816">+374 94 86 18 16</a></span>
</div>
<div class="detail-item">
<strong>Email:</strong>
<span><a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Сайт:</strong>
<span><a href="https://dexarmarket.ru" target="_blank">dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Часы работы офиса:</strong>
<span>10:00 - 19:00 (МСК)</span>
</div>
<div class="detail-item">
<strong>Техподдержка:</strong>
<span>24/7</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Руководство</h2>
<p><strong>Генеральный директор:</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Основание действий:</strong> Устав</p>
</section>
</div>
</div>

View File

@@ -1,119 +0,0 @@
<div class="legal-page">
<div class="legal-container">
<h1>Правила оплаты</h1>
<section class="legal-section">
<h2>1. Общие положения</h2>
<p>1.1. Настоящие Правила определяют порядок оплаты Товаров и Услуг, приобретаемых Покупателями через Маркетплейс DexarMarket.</p>
<p>1.2. Оплата производится за Товары/Услуги, размещенные независимыми Продавцами. Маркетплейс выступает в качестве информационного посредника и обеспечивает техническую инфраструктуру для проведения платежей.</p>
<p>1.3. Оплата товаров и услуг на Маркетплейсе осуществляется в российских рублях (RUB).</p>
<p>1.4. Цены на Товары/Услуги устанавливаются Продавцами самостоятельно и указываются на странице соответствующего Товара/Услуги.</p>
</section>
<section class="legal-section">
<h2>2. Способы оплаты</h2>
<p>2.1. Маркетплейс поддерживает следующие способы оплаты:</p>
<div class="payment-methods">
<div class="payment-logos">
<img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="80" height="50" />
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="80" height="50" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="80" height="50" />
</div>
</div>
<ul>
<li><strong>Банковские карты:</strong> Visa, Mastercard, МИР</li>
<li><strong>Системы быстрых платежей (СБП):</strong> мгновенный перевод через мобильное приложение банка</li>
<li><strong>Электронные кошельки:</strong> ЮMoney, QIWI (при наличии)</li>
<li><strong>Оплата по ссылке:</strong> генерация уникальной платежной ссылки для каждого заказа</li>
</ul>
<p>2.2. Доступные способы оплаты могут различаться в зависимости от Продавца и типа Товара/Услуги.</p>
<p>2.3. Все платежи обрабатываются через сертифицированные платежные системы с соблюдением стандартов безопасности PCI DSS.</p>
</section>
<section class="legal-section">
<h2>3. Процесс оплаты</h2>
<p>3.1. Процедура оплаты заказа включает следующие этапы:</p>
<ol>
<li>Выбор Товаров/Услуг и добавление их в корзину</li>
<li>Оформление заказа с указанием контактных данных и способа доставки (при необходимости)</li>
<li>Выбор способа оплаты из доступных вариантов</li>
<li>Перенаправление на защищенную страницу платежной системы или получение платежной ссылки</li>
<li>Ввод платежных данных и подтверждение оплаты</li>
<li>Получение уведомления об успешной оплате</li>
</ol>
<p>3.2. При оплате банковской картой Покупатель может быть перенаправлен на страницу банка-эмитента для прохождения дополнительной аутентификации (3D-Secure).</p>
<p>3.3. Обязательство Покупателя по оплате считается исполненным с момента поступления денежных средств на счет платежной системы.</p>
</section>
<section class="legal-section">
<h2>4. Безопасность платежей</h2>
<p>4.1. Все платежи обрабатываются через защищенное HTTPS-соединение с использованием протокола TLS 1.2 и выше.</p>
<p>4.2. Маркетплейс не хранит полные данные банковских карт Покупателей. Обработка платежных данных осуществляется сертифицированными платежными агрегаторами.</p>
<p>4.3. Для защиты от мошенничества применяется технология 3D-Secure, требующая подтверждения платежа через SMS-код или push-уведомление от банка.</p>
<p>4.4. В случае подозрительной активности платежная система имеет право запросить дополнительную верификацию личности Покупателя.</p>
</section>
<section class="legal-section">
<h2>5. Подтверждение оплаты</h2>
<p>5.1. После успешной оплаты Покупатель получает подтверждение на указанный при оформлении заказа адрес электронной почты.</p>
<p>5.2. Подтверждение содержит следующую информацию:</p>
<ul>
<li>Номер заказа</li>
<li>Дата и время оплаты</li>
<li>Сумма платежа</li>
<li>Состав заказа</li>
<li>Контактные данные Продавца</li>
</ul>
<p>5.3. Информация о заказе также отображается в личном кабинете Покупателя на Маркетплейсе (при наличии регистрации).</p>
<p>5.4. Фискальный чек направляется Продавцом в соответствии с требованиями законодательства РФ.</p>
</section>
<section class="legal-section">
<h2>6. Возврат средств</h2>
<p>6.1. Порядок возврата денежных средств регулируется <a routerLink="/return-policy">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p>
<p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p>
<p>6.3. Срок возврата денежных средств составляет:</p>
<ul>
<li>На банковскую карту: от 3 до 30 банковских дней (в зависимости от банка-эмитента)</li>
<li>На электронный кошелек: от 1 до 5 рабочих дней</li>
<li>Через СБП: от 1 до 3 рабочих дней</li>
</ul>
<p>6.4. За обработку возврата средств Маркетплейс комиссию не взимает. Комиссии платежных систем и банков могут применяться в соответствии с их тарифами.</p>
</section>
<section class="legal-section">
<h2>7. Неуспешные платежи</h2>
<p>7.1. Платеж может быть отклонен по следующим причинам:</p>
<ul>
<li>Недостаточно средств на счете</li>
<li>Неверно введены платежные данные</li>
<li>Карта заблокирована или просрочена</li>
<li>Превышены лимиты на операции, установленные банком</li>
<li>Отказ в проведении транзакции системой безопасности</li>
</ul>
<p>7.2. В случае неуспешной оплаты Покупатель получает уведомление с указанием причины отказа.</p>
<p>7.3. При возникновении проблем с оплатой рекомендуется:</p>
<ul>
<li>Проверить правильность введенных данных</li>
<li>Связаться с банком-эмитентом карты для уточнения причины отказа</li>
<li>Попробовать альтернативный способ оплаты</li>
<li>Обратиться в службу поддержки: <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></li>
</ul>
</section>
<section class="legal-section">
<h2>8. Контакты для вопросов по оплате</h2>
<p>По вопросам, связанным с оплатой заказов, вы можете обратиться:</p>
<ul>
<li><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></li>
<li><strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></li>
<li><strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a></li>
<li><strong>Время работы:</strong> Круглосуточно (техническая поддержка)</li>
<li><strong>Среднее время ответа:</strong> До 24 часов в рабочие дни</li>
</ul>
<p>При обращении указывайте номер заказа и краткое описание проблемы для более быстрого решения вопроса.</p>
</section>
</div>
</div>

View File

@@ -1,258 +0,0 @@
<div class="legal-page">
<div class="legal-container">
<h1>ПОЛИТИКА В ОТНОШЕНИИ ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ</h1>
<section class="legal-section">
<h2>1. ОБЩИЕ ПОЛОЖЕНИЯ</h2>
<p>1.1. Настоящая политика ООО "ИНТ ФИН ЛОГИСТИК" (ИНН 9909697628), именуемого далее как «Оператор», описывает порядок обработки персональных данных и направлена на защиту прав и законных интересов субъектов данных. Документ разработан в соответствии с Федеральным законом №152-ФЗ от 27 июля 2006 года «О персональных данных».</p>
<p>1.2. Политика определяет порядок и меры обеспечения безопасности обработки персональных данных на сайте <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>, ставя своей задачей защитить права и свободы человека и гражданина, включая право на неприкосновенность частной жизни, личную и семейную тайны.</p>
<p>1.3. Документ охватывает все процессы, осуществляемые Оператором, касающиеся обработки персональных данных.</p>
<p>1.4. Политика обязательна для изучения и выполнения всеми лицами, допущенными к обработке персональных данных.</p>
<p>1.5. Действует в отношении всех действий, связанных с обработкой персональных данных на сайте <a href="https://dexarmarket.ru">https://dexarmarket.ru</a> и в информационных системах Оператора.</p>
<p>1.6. Пользователь, оформляющий заказ, открывающий личный кабинет или иным образом взаимодействующий с Оператором, выражает согласие на обработку своих персональных данных в соответствии с Политикой и законодательством РФ. Продолжительное использование сайта свидетельствует о согласии с Положениями Политики. Пользователь, не готовый согласиться с условиями, должен воздержаться от использования ресурса.</p>
<p><strong>Дополнительно:</strong> Настоящая Политика распространяется на персональные данные, собранные как до вступления документа в силу, так и после.</p>
</section>
<section class="legal-section">
<h2>2. ТЕРМИНЫ И ОПРЕДЕЛЕНИЯ</h2>
<p>В данной Политике используются следующие основные термины и определения:</p>
<p><strong>Персональные данные (ПДн)</strong> — любая информация, прямо или косвенно относящаяся к определённому физическому лицу (субъекту персональных данных).</p>
<p><strong>Информационная система персональных данных (ИСПДн)</strong> — совокупность персональных данных, хранимых в базах данных, а также технологий и средств для их обработки.</p>
<p><strong>Автоматизированная обработка ПДн</strong> — обработка данных с использованием компьютерных средств.</p>
<p><strong>Блокировка ПДн</strong> — временная приостановка обработки данных (за исключением случаев уточнения данных).</p>
<p><strong>Обезличивание ПДн</strong> — действия, ведущие к невозможности установления принадлежности данных конкретному лицу без дополнительной информации.</p>
<p><strong>Интернет-сайт (Сайт)</strong> — автоматизированная информационная система, доступная в сети Интернет по адресу: <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>.</p>
<p><strong>Обработка ПДн</strong> — любые действия с персональными данными, включая сбор, запись, хранение, обновление, использование, передачу, уничтожение и другие операции.</p>
<p><strong>Оператор</strong> — государственный или частный орган, самостоятельно или совместно организующий обработку персональных данных.</p>
<p><strong>Предоставление ПДн</strong> — передача данных определённому лицу или группе лиц.</p>
<p><strong>Распространение ПДн</strong> — открытие данных неопределённому кругу лиц, включая публикацию в СМИ или сети Интернет.</p>
<p><strong>Трансграничная передача ПДн</strong> — передача данных зарубежным властям, компаниям или физическим лицам.</p>
<p><strong>Уничтожение ПДн</strong> — действия, приводящие к утрате возможности восстановления данных или уничтожения материальных носителей.</p>
<p><strong>Субъект ПДн</strong> — физическое лицо, чья информация обрабатывается.</p>
<p><strong>Конфиденциальность ПДн</strong> — обязанность Оператора защищать данные от распространения без согласия субъекта или законного основания.</p>
<p><strong>Продавец (Исполнитель)</strong> — лицо, предлагающее товары или услуги на сайте <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>.</p>
<p><strong>Пользователь</strong> — лицо, посещающее или использующее ресурсы, управляемые Оператором, включая сайт <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>.</p>
<p><strong>Заказ</strong> — оформленный Пользователем заказ товаров или услуг на сайте.</p>
<p><strong>Файлы cookie</strong> — небольшие файлы, сохраняемые на устройстве пользователя для запоминания предпочтений и действий при последующих посещениях сайта.</p>
</section>
<section class="legal-section">
<h2>3. ПРАВОВЫЕ ОСНОВАНИЯ ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
<p>3.1. Правовым основанием Обработки ПДн в зависимости от целей процесса, предусматривающего Обработку ПДн может являться:</p>
<h3>3.1.1. Конституция Российской Федерации, а также совокупность правовых актов:</h3>
<ul>
<li>Налоговый кодекс Российской Федерации;</li>
<li>Гражданский Кодекс Российской Федерации;</li>
<li>ст. 86 - 90 Трудового кодекса Российской Федерации;</li>
<li>Федеральный закон от 07.08.2001 № 115-ФЗ «О противодействии легализации (отмыванию) доходов, полученных преступным путем, и финансированию терроризма»;</li>
<li>Федеральный закон от 27.07.2006 г. № 152-ФЗ «О персональных данных»;</li>
<li>Федеральный закон от 22.04.1996 № 39-ФЗ «О рынке ценных бумаг»;</li>
<li>Федеральный закон от 26.12.1995 №208-ФЗ «Об акционерных обществах»;</li>
<li>Федеральный закон от 27.07.2006 № 149-ФЗ «Об информации, информационных технологиях и о защите информации»;</li>
<li>Федеральный закон от 01.04.1996 № 27-ФЗ «Об индивидуальном (персонифицированном) учете в системе обязательного пенсионного страхования»;</li>
<li>Федеральный закон от 06.04.2011 № 63-ФЗ «Об электронной подписи»;</li>
<li>Федеральный закон от 06.12.2011 № 402-ФЗ «О бухгалтерском учете»;</li>
<li>Федеральный закон от 27.06.2011 № 161-ФЗ «О национальной платежной системе»;</li>
<li>Постановление Правительства Российской Федерации от 15.09.2008 № 687 «Об утверждении положения об особенностях обработки персональных данных, осуществляемой без использования средств автоматизации»;</li>
<li>Постановлением Правительства РФ от 01.11.2012 № 1119 "Об утверждении требований к защите персональных данных при их обработке в информационных системах персональных данных"</li>
<li>иные нормативные правовые акты Российской Федерации и нормативные документы исполнительных органов государственной власти.</li>
</ul>
<p>3.1.2. Устав Оператора.</p>
<p>3.1.3. Договоры, заключаемые между Оператором и Субъектом Персональных данных, в том числе в случае реализации Оператором своего права на уступку прав (требований) по таким договорам, между Оператором и иным лицом, поручившим Оператору Обработку ПДн, а также для заключения договоров, стороной которых являются Субъекты Персональных данных.</p>
<p>3.1.4. Согласие на Обработку ПДн (в случаях, прямо не предусмотренных законодательством Российской Федерации, но соответствующих полномочиям Оператора), в т.ч. согласие соискателей на замещение вакантных должностей на Обработку ПДн, согласие практиканта на Обработку ПДн, согласие работников на Обработку ПДн; согласие клиентов на Обработку ПДн, согласие Пользователей соответствующего Сайта, согласие иных Субъектов Персональных данных.</p>
<p>3.1.5. Договор между оператором и третьим лицом, где последнее поручает Оператору обработку персональных данных Субъекта Персональных данных или передает на основании заключенного договора персональные данные Субъекта Персональных данных.</p>
</section>
<section class="legal-section">
<h2>4. КАТЕГОРИИ СУБЪЕКТОВ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
<p>4.1. Оператором осуществляется Обработка полученных в установленном законом порядке ПДн, принадлежащих:</p>
<ul>
<li>Кандидатам на работу и работникам Оператора;</li>
<li>Уволенным работникам Оператора;</li>
<li>Близким родственникам/членам семьи работников Оператора, практикантам;</li>
<li>Потенциальным клиентам, клиентам - физическим лицам, клиентам - индивидуальным предпринимателям;</li>
<li>Физическим лицам, заключившим с Оператором гражданско-правовые договоры;</li>
<li>Пользователям Сайтов Оператора, получателям Заказов;</li>
<li>Клиентам других юридических лиц;</li>
<li>Собственникам Оператора;</li>
<li>Иным субъектам, вступившим или намеревающимися вступить в договорные отношения с Оператором.</li>
</ul>
</section>
<section class="legal-section">
<h2>5. КАТЕГОРИИ ОБРАБАТЫВАЕМЫХ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
<p>5.1. Оператор обрабатывает следующие категории ПДн Пользователей:</p>
<ul>
<li>Сведения, полученные при регистрации и/или оформлении Заказа (фамилию, имя, фактический адрес, номер телефона, адрес электронной почты, cookie);</li>
<li>Сведения, полученные при взаимодействии с Пользователями (фамилия, имя, отчество, пол, место рождения, дата рождения, паспортные данные, адрес, контакты);</li>
<li>Сведения о способе доставки и оплаты Заказа;</li>
<li>Сведения о претензиях Пользователя;</li>
<li>Сведения о геолокации (местонахождении).</li>
</ul>
<p>5.2. Персональные данные могут быть получены Оператором путем:</p>
<ul>
<li>Предоставления Субъектами через формы на Сайте или по email;</li>
<li>Получения от третьих лиц (Продавцов, контрагентов) в рамках законодательства РФ.</li>
</ul>
</section>
<section class="legal-section">
<h2>6. ПРИНЦИПЫ, ПОРЯДОК И УСЛОВИЯ ОБРАБОТКИ</h2>
<h3>6.1. Принципы обработки Персональных данных</h3>
<p>Обработка ПДн у Оператора осуществляется на основе следующих принципов:</p>
<ul>
<li>законности и справедливой основы;</li>
<li>ограничения Обработки ПДн достижением конкретных, заранее определенных и законных целей;</li>
<li>недопущения Обработки ПДн, несовместимой с целями сбора ПДн;</li>
<li>обеспечения точности, достаточности и актуальности ПДн;</li>
<li>хранение ПДн не дольше, чем этого требуют цели Обработки;</li>
<li>уничтожения либо обезличивания ПДн по достижении целей их Обработки.</li>
</ul>
<h3>6.2. Обязанности работников Оператора</h3>
<p>Работники Оператора, допущенные к Обработке Персональных данных, обязаны:</p>
<ul>
<li>Знать и выполнять положения законодательства РФ в области ПДн;</li>
<li>Знать и выполнять положения настоящей Политики;</li>
<li>Обрабатывать ПДн только в рамках должностных обязанностей;</li>
<li>Не разглашать ПДн, обрабатываемые Оператором;</li>
<li>Сообщать о действиях, которые могут привести к нарушению Политики.</li>
</ul>
</section>
<section class="legal-section">
<h2>7. ПРАВА СУБЪЕКТА ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
<h3>7.1. Согласие Субъекта</h3>
<p>Субъект ПДн дает согласие на Обработку свободно, своей волей и в своем интересе.</p>
<h3>7.2. Права Субъекта</h3>
<p>Субъект ПДн имеет право на получение информации о:</p>
<ul>
<li>подтверждение факта Обработки ПДн;</li>
<li>правовые основания и цели Обработки;</li>
<li>применяемые способы Обработки;</li>
<li>сроки Обработки и хранения данных;</li>
<li>уточнение, блокирование или уничтожение данных.</li>
</ul>
<p><strong>Субъект ПДн имеет право отозвать свое Согласие</strong> и потребовать удалить свои ПДн, направив сообщение на: <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></p>
<p>Субъект также имеет право на защиту своих прав, возмещение убытков и компенсацию морального вреда.</p>
</section>
<section class="legal-section">
<h2>8. ОБЯЗАННОСТИ ОПЕРАТОРА</h2>
<p>8.1. Оператор обязан предоставить Субъекту информацию о обработке его данных при обращении.</p>
<p>8.2. Оператор обеспечивает хранение данных граждан РФ на территории Российской Федерации.</p>
<p>8.3. Оператор несет иные обязанности, установленные ФЗ-152.</p>
</section>
<section class="legal-section">
<h2>9. ОБЕСПЕЧЕНИЕ БЕЗОПАСНОСТИ</h2>
<p>9.1-9.3. Безопасность обеспечивается комплексом организационных и технических мер:</p>
<ul>
<li>назначение ответственных лиц;</li>
<li>ограничение доступа к ПДн;</li>
<li>обучение сотрудников;</li>
<li>учет и хранение носителей с ПДн;</li>
<li>использование антивирусных средств;</li>
<li>средства шифрования и межсетевого экранирования;</li>
<li>физическая защита помещений;</li>
<li>контроль мер безопасности.</li>
</ul>
</section>
<section class="legal-section">
<h2>10. ОТВЕТСТВЕННОСТЬ</h2>
<p>10.1. Лица, виновные в нарушении норм обработки и защиты Персональных данных, несут ответственность согласно законодательству РФ.</p>
</section>
<section class="legal-section">
<h2>11. ЦЕЛИ ОБРАБОТКИ</h2>
<p>11.1. Оператор обрабатывает ПДн для следующих целей:</p>
<ul>
<li>Предоставления доступа к Сайту и личному кабинету;</li>
<li>Исполнения договоров купли-продажи и оказания услуг;</li>
<li>Доставки товаров;</li>
<li>Урегулирования претензий;</li>
<li>Обработки платежей;</li>
<li>Улучшения качества обслуживания;</li>
<li>Получения обратной связи;</li>
<li>Проведения маркетинговых исследований;</li>
<li>Направления рекламных сообщений;</li>
<li>В иных случаях, предусмотренных законодательством РФ.</li>
</ul>
</section>
<section class="legal-section">
<h2>12. АВТОМАТИЧЕСКИ СОБИРАЕМАЯ ИНФОРМАЦИЯ</h2>
<p>12.1. Оператор собирает и обрабатывает:</p>
<ul>
<li>информацию об интересах на основе поисковых запросов;</li>
<li>информацию для формирования рейтинга (отзывы, данные об исполнении Заказов);</li>
<li>статистику об использовании Сайта.</li>
</ul>
<p>12.2. Оператор использует cookies, веб-отметки и другие технологии мониторинга. Эти технологии не дают возможность автоматически получать ПДн.</p>
<p>12.3. Если собранные сведения можно соотнести с личным кабинетом Пользователя, они могут обрабатываться совместно с ПДн.</p>
</section>
<section class="legal-section">
<h2>13. КОНТАКТНАЯ ИНФОРМАЦИЯ</h2>
<p>По всем вопросам обработки персональных данных обращайтесь:</p>
<ul>
<li><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></li>
<li><strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></li>
<li><strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a></li>
</ul>
<p>Мы ответим в течение 30 дней согласно законодательству РФ.</p>
</section>
</div>
</div>

View File

@@ -1,467 +0,0 @@
<div class="legal-page">
<div class="legal-container">
<h1>СОГЛАШЕНИЕ ПУБЛИЧНОЙ ОФЕРТЫ</h1>
<section class="legal-section">
<h2>Основные понятия</h2>
<p>В данном документе приводятся ключевые термины и пояснения, применяемые в публичном соглашении:</p>
<ul>
<li><strong>Маркетплейс, Интернет-сайт или Сайт</strong> — это комплекс технических и программных средств, предназначенный для функционирования веб-ресурса по адресу <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>. Сайт принадлежит владельцу и служит платформой для организации взаимодействия между независимыми продавцами и покупателями. Сам владелец сайта не занимается продажей товаров и услуг, а предоставляет пространство для размещения объявлений и заключения сделок.</li>
<li><strong>Пользователь</strong> — физлицо, которое пользуется сайтом, регистрируясь и принимая условия данного соглашения.</li>
<li><strong>Личный кабинет</strong> — персональная страница Пользователя, защищённая индивидуальными регистрационными данными (логином и паролем). Логином может выступать электронная почта или мобильный телефон, используемый для авторизации.</li>
<li><strong>Учетные данные</strong> — комбинация имени пользователя (логина) и пароля, создаваемая при регистрации.</li>
<li><strong>Покупатель</strong> — зарегистрированный Пользователь, который сделал заказ через сайт.</li>
<li><strong>Администратор или Владелец Сайта</strong> — юридическое лицо ООО «ИНТ ФИН ЛОГИСТИК», идентификационный налоговый номер (ИНН) 9909697628.</li>
<li><strong>Продавец или Селлер</strong> — физические или юридические лица, предприниматели, предлагающие товары и услуги на ресурсе. Продавцы несут личную ответственность за качество, безопасность и соответствие описания товаров.</li>
<li><strong>Контент Сайта</strong> — совокупные материалы, находящиеся на ресурсе, включая дизайн, тексты, графику, видео, аудиозаписи и прочие объекты.</li>
<li><strong>Товар</strong> — материальные и цифровые продукты, а также услуги, предлагаемые на продажу через сайт.</li>
<li><strong>Цифровой товар</strong> — товары, распространяемые в электронной форме, такие как ПО, подписка на онлайн-курсы, виртуальные активы, музыкальные треки, электронные книги и подобные виды продукции.</li>
<li><strong>Заказ</strong> — оформленное заявление на приобретение товаров или услуг от Покупателя.</li>
<li><strong>Сервис</strong> — специальное программное обеспечение, интегрированное в ресурс, дающее возможность пользоваться функциональностью сайта.</li>
<li><strong>Продукты Владельца Сайта</strong> — информационные и сопутствующие услуги, предоставляемые владельцем сайта.</li>
</ul>
</section>
<section class="legal-section">
<h2>1. Общие положения</h2>
<p>1.1. Настоящим документом устанавливается порядок использования ресурсов, принадлежащих администратору, включая сайт, мобильные версии и приложения, управляемые Администрацией.</p>
<p>1.2. Документ является обязательным соглашением между Пользователем и Владельцем ресурса.</p>
<p>1.3. Настоящее соглашение и информация о продуктах, размещённая на сайте, соответствуют определению публичной оферты согласно статье 435 и пункту 2 статьи 437 ГК РФ.</p>
<p>1.4. Пользователь соглашается с условиями договора автоматически, начиная с первого посещения сайта, регистрации или оформления заказа без авторизации, нажатия кнопок «Оформить заказ», «Отправить сообщение» или «Купить».</p>
<p>1.5. Заключение соглашения путём акцептования оферты не требует подписи сторон и признается действительным в электронном виде.</p>
<p>1.6. Использование сайта подразумевает согласие с условиями соглашения, вступающего в силу сразу после выражения согласия пользователем.</p>
<p>1.7. В случае несогласия с условиями Пользователь обязуется незамедлительно прекратить пользование ресурсом.</p>
<p>1.8. Дополнительно регулирование использования сайта осуществляется <a routerLink="/privacy-policy">Политикой обработки персональных данных</a>.</p>
<p>1.9. Изменения в соглашение могут вноситься Владельцем без предварительного уведомления и становятся обязательными с момента публикации изменений.</p>
<p>1.10. Во время проведения рекламных кампаний могут устанавливаться особые условия оформления заказов, возврата или обмена товаров.</p>
<p>1.14. Оферта предназначена для любых пользователей ресурса, включая юридических лиц и индивидуальных предпринимателей.</p>
</section>
<section class="legal-section">
<h2>2. Предмет соглашения</h2>
<p>2.1. Целью соглашения является предоставление возможности Пользователям покупать товары и услуги, представляемые на ресурсе, а также использование продуктов, предоставляемых Владельцем сайта.</p>
<p>2.2. Соглашение регулирует порядок использования сайта и функций, предоставляемых Владельцем.</p>
<p>2.3. Действие документа распространяется на все типы товаров, услуг и продуктов, имеющихся на сайте.</p>
</section>
<section class="legal-section">
<h2>3. Условия продажи товаров и оказания услуг</h2>
<p><strong>3.1. Принятие условий продажи</strong></p>
<p>Пользователь, делая заказы через маркетплейс, выражает полное согласие с условиями продажи товаров и оказания услуг, определёнными в данном соглашении.</p>
<p><strong>3.2. Заключение договоров</strong></p>
<p>Договор розничной купли-продажи или договор оказания услуг заключается непосредственно между Продавцом и Покупателем с момента выдачи Продавцом кассовых или товарных чеков, подтверждающих оплату. Маркетплейс играет роль информационного посредника, предоставляя инфраструктуру для совершения сделок, но не является участником данного договора. Ответственность за выполнение договора, качество товаров и услуг лежит на Продавце.</p>
<p><strong>3.3. Согласие на обработку контактов</strong></p>
<p>Пользователь даёт согласие на использование его контактных данных (адрес электронной почты, номер телефона) администрацией сайта и Продавцом, а также привлечёнными третьими сторонами для выполнения обязательств перед Покупателем, включая рассылки рекламной и иной информации.</p>
<p><strong>3.4. Привлечение третьих лиц</strong></p>
<p>Пользователь соглашается с возможностью привлечения Продавцом третьих лиц для исполнения договора купли-продажи или оказания услуг, сохраняя ответственность Продавца за выполнение обязательств.</p>
<p><strong>3.5. Права и обязательства по договорам</strong></p>
<p>Права и обязанности по договорам купли-продажи возникают непосредственно у Продавца. Пользователь осознаёт, что Владелец сайта исполняет роль оператора маркетплейса и не несёт ответственности за действия Продавцов, качество товаров, законное основание Продавца осуществлять сделки и фактическое исполнение обязательств.</p>
<p><strong>3.6. Функции владельца сайта</strong></p>
<p>Владелец сайта предоставляет информационное и техническое сопровождение: координирует взаимодействие между Покупателем и Продавцом, передает информацию о заказе, может помогать в урегулировании споров. Тем не менее, ответственность за исполнение договора остаётся за Продавцом.</p>
<p><strong>3.7. Переуступка прав Продавцом</strong></p>
<p>Покупатель осведомлён и соглашается с правом Продавца уступать или передавать свои права и обязанности третьим сторонам.</p>
<p><strong>3.8. Применение законов</strong></p>
<p>Отношения между Пользователем и Продавцом регулируются положениями Федерального закона «О защите прав потребителей» (№ 2300-1 от 07 февраля 1992 года) и Гражданского кодекса РФ.</p>
<p><strong>3.10. Рекламные сообщения</strong></p>
<p>Пользователь дает согласие на получение рекламных сообщений в соответствии с Федеральным законом «О рекламе».</p>
</section>
<section class="legal-section">
<h2>4. Регистрация на сайте и Личный кабинет Пользователя</h2>
<p><strong>4.1. Процедуры регистрации</strong></p>
<p>Пользователю предлагается зарегистрироваться на сайте для полноценного использования отдельных функций и сервисов. Хотя регистрация необязательна для оформления заказа, она создаёт уникальные возможности, включая Личный кабинет.</p>
<p><strong>4.2. Обязанности Пользователя при регистрации</strong></p>
<p>Регистрируясь, Пользователь берёт обязательство предоставлять точную и актуальную информацию, заполняя анкету регистрации. Необходимо поддерживать данные в актуальном состоянии.</p>
<p><strong>4.3. Идентификация пользователя</strong></p>
<p>Любые действия, совершённые с использованием индивидуального логина и пароля, считаются действиями самого Пользователя, пока не доказана противоположная позиция.</p>
<p><strong>4.4. Конфиденциальность учетных данных</strong></p>
<p>Пользователь обязуется держать в тайне логин и пароль, предоставленные при регистрации. При подозрении на нарушение безопасности необходимо срочно проинформировать администрацию сайта письмом на адрес <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a>.</p>
<p><strong>4.7. Подтверждение данных</strong></p>
<p>Владелец сайта имеет право запросить подтверждение предоставленной информации в любой момент.</p>
<p><strong>4.11. Завершение сеанса работы</strong></p>
<p>Пользователь обязан самостоятельно завершать работу в Личном кабинете («Выход»), покидая сайт, для обеспечения безопасности аккаунта.</p>
</section>
<section class="legal-section">
<h2>5. Права и обязанности Владельца сайта</h2>
<p><strong>5.1. Права Владельца сайта</strong></p>
<p>Владелец сайта наделён следующими полномочиями:</p>
<ul>
<li>Устанавливать ограничения в использовании ресурса для всех или отдельных групп пользователей.</li>
<li>Направлять Пользователям информацию о нововведениях и изменениях на сайте.</li>
<li>Передавать полномочия третьим лицам без одобрения Покупателя.</li>
<li>Изменять условия проводимых акций в одностороннем порядке.</li>
<li>Ограничивать действия Пользователей, создающих риски для работоспособности сайта.</li>
<li>Проводить технические работы без предупреждения пользователей.</li>
<li>Использовать статистику, собранную на сайте, в собственных интересах.</li>
<li>Менять список товаров и услуг, ценники и условия в любое время.</li>
<li>Отказывать в обслуживании Пользователям, подозреваемым в неправомерных действиях.</li>
</ul>
<p><strong>5.2. Обязанности Владельца сайта</strong></p>
<p>Основной обязанностью Владельца сайта является оказание Пользователям услуг, предусмотренных пунктом 2.1 настоящего соглашения.</p>
</section>
<section class="legal-section">
<h2>6. Права и обязанности Пользователя</h2>
<p><strong>6.1. Обязанности Пользователя</strong></p>
<p>Пользователь обязуется соблюдать ряд важных обязанностей:</p>
<ul>
<li>Ознакомляться с текстом соглашения до момента заключения договора купли-продажи или оказания услуг.</li>
<li>Придерживаться этического поведения при написании отзывов и взаимодействии с персоналом сайта.</li>
<li>Общаться вежливо и уважительно с сотрудниками и коллегами.</li>
<li>Не допускать распространение ненормативной лексики, грубостей и оскорблений.</li>
<li>Не создавать помех нормальной работе сайта и его сервисов.</li>
<li>Не заниматься загрузкой вирусов, вредоносных программ и других опасных файлов.</li>
<li>Не применять запрещённые автоматизированные программы для сбора информации без разрешения Администрации.</li>
<li>Не пытаться получить доступ к чужим учётным данным.</li>
<li>Законно использовать контент сайта и избегать незаконного копирования материалов.</li>
<li>Зарегистрироваться только от собственного имени и не маскироваться под других лиц.</li>
<li>Своевременно оплачивать заказанные товары и услуги в соответствии с соглашением.</li>
<li>Использовать приобретённые товары исключительно для личных нужд, не преследуя коммерческих целей.</li>
<li>Беречь информационную безопасность ресурса, воздерживаясь от попыток взлома или нарушения целостности сайта.</li>
</ul>
<p><strong>6.2. Запрещённые действия Пользователя</strong></p>
<p>Пользователю категорически запрещается:</p>
<ul>
<li>Загрузка и публикация контента, противоречащего закону, содержащего вирусные программы, ложную информацию или уничижительную критику.</li>
<li>Нарушение прав других пользователей и нанесение вреда несовершеннолетним гражданам.</li>
<li>Любое поведение, нарушающее российское законодательство, включая нормы международного права.</li>
</ul>
<p><strong>6.3. Права Пользователя</strong></p>
<p>Пользователь имеет право отказаться от получения рекламных сообщений, воспользовавшись соответствующим инструментом на сайте или направив заявку по электронной почте <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> или письмом по официальному адресу Владельца сайта.</p>
</section>
<section class="legal-section">
<h2>7. Исключительные права на контент сайта</h2>
<p><strong>7.1. Объекты интеллектуальной собственности</strong></p>
<p>Весь контент, доступный на сайте, включая:</p>
<ul>
<li>элементы дизайна,</li>
<li>текстовые материалы,</li>
<li>графики и изображения,</li>
<li>видеоролики,</li>
<li>компьютерные программы,</li>
<li>базы данных,</li>
<li>другие материалы —</li>
</ul>
<p>является объектом исключительных авторских прав Владельца сайта и других правообладателей.</p>
<p><strong>7.2. Использование контента</strong></p>
<p>Применение контента разрешено только в рамках функциональных возможностей сайта. Любое другое использование элементов сайта или размещённого контента без предварительного разрешения владельцев недопустимо.</p>
<p><strong>7.3. Использование для личного потребления</strong></p>
<p>Пользователь вправе использовать контент сайта для личного некоммерческого использования при условии обязательного сохранения обозначений авторских прав, смежных прав, товарных знаков и прочих уведомлений об авторстве.</p>
</section>
<section class="legal-section">
<h2>8. Сайты и контент третьих лиц</h2>
<p><strong>8.1. Наличие ссылок на внешние ресурсы</strong></p>
<p>Сайт может включать гиперссылки на сторонние ресурсы в Интернете. Эти ресурсы не подвергаются проверке Владельцем сайта на предмет соответствия законодательным нормам или другим критериям. Соответственно, ответственность за содержание и материалы на сайтах третьих лиц не относится к компетенции Владельца сайта.</p>
<p><strong>8.2. Характер ссылок</strong></p>
<p>Ссылки на сторонние сайты, а также упоминания продуктов, услуг или иной информации коммерческого или некоммерческого характера, присутствующие на сайте, не означают одобрения или рекомендаций со стороны Владельца сайта.</p>
<p><strong>8.3. Требования к цитированию</strong></p>
<p>При любом воспроизведении материалов сайта необходимо указывать активную ссылку на источник.</p>
<p><strong>8.4. Интеллектуальная собственность</strong></p>
<p>Логотип, фирменное название, визуальное оформление и общий облик сайта принадлежат интеллектуальным правам Владельца сайта. Их использование без прямой явной санкции от Владельца запрещено.</p>
</section>
<section class="legal-section">
<h2>9. Ответственность сторон</h2>
<p><strong>9.1. Общая ответственность</strong></p>
<p>Пользователь использует сайт на свой риск. Ресурсы предоставляются "как есть", без гарантий соответствия ожиданиям пользователя.</p>
<p><strong>9.2. Ограничения ответственности</strong></p>
<p>Владелец сайта не гарантирует:</p>
<ul>
<li>Соответствие сайта потребностям пользователя.</li>
<li>Постоянную доступность, скорость и отсутствие ошибок в сервисе.</li>
</ul>
<p><strong>9.3. Последствия использования информации</strong></p>
<p>Вся информация и материалы, получаемые через сайт, используются пользователем на его усмотрение и риск. Пользователь несёт ответственность за возможные негативные последствия использования этих сведений.</p>
<p><strong>9.4. Ответственность за недостоверную информацию</strong></p>
<p>Владелец сайта не несёт ответственности за неверные или неточные данные, предоставленные пользователем при регистрации.</p>
<p><strong>9.5. Безопасность аккаунта</strong></p>
<p>Пользователь самостоятельно отвечает за сохранение секретности используемых средств доступа к своему профилю.</p>
<p><strong>9.6. Ответственность Продавца</strong></p>
<p>Продавец несёт полную ответственность за качество, безопасность и соответствие реализуемой продукции заявленным характеристикам, а также за убытки, вызванные нарушением обязательств перед Покупателем.</p>
<p><strong>9.7. Освобождение Владельца сайта от ответственности</strong></p>
<p>Владелец сайта не несёт ответственности за:</p>
<ul>
<li>Качество, безопасность и правомерность товаров и услуг, предлагаемых Продавцами.</li>
<li>Выполнение или ненадлежащее выполнение Продавцами своих обязательств перед Покупателями.</li>
<li>Истинность информации, представляемой Продавцами о товаре.</li>
<li>Повреждение прав третьих лиц, включая интеллектуальную собственность.</li>
<li>Вопросы доставки, комплектации и состояния товаров.</li>
</ul>
<p>Покупатель соглашается, что претензии по качеству, количеству, комплектности товаров и услугам следует направлять непосредственно Продавцу. Администрация сайта может способствовать решению споров, но сама ответственности не несёт.</p>
<p><strong>9.8. Ответственность за доставку</strong></p>
<p>Ответственность за сроки, условия и качество доставки товаров несут транспортные компании и курьерские службы. Владелец сайта выступает только в качестве информационного посредника и не несёт ответственности за действия служб доставки.</p>
<p><strong>9.9. Ответственность Пользователя перед третьими лицами</strong></p>
<p>Пользователь лично отвечает перед третьими лицами за свои действия, связанные с использованием сайта.</p>
<p><strong>9.10. Форс-мажорные обстоятельства</strong></p>
<p>Работа сайта может быть приостановлена в случае непредвиденных обстоятельств, аварий, сбоев оборудования или вмешательств третьих лиц без предварительных уведомлений.</p>
<p><strong>9.11. Ответственность за рекламу</strong></p>
<p>Ответственность за содержание рекламных объявлений несут Продавцы, размещающие информацию о своих товарах и услугах. Владелец сайта предоставляет только техническую платформу для размещения информации.</p>
<p><strong>9.12. Стихийные бедствия и чрезвычайные события</strong></p>
<p>Ни одна сторона не несёт ответственности за невыполнение обязательств, вызванное обстоятельствами непреодолимой силы, такими как природные катастрофы, войны или техногенные аварии.</p>
</section>
<section class="legal-section">
<h2>10. Товар и порядок совершения покупки</h2>
<p><strong>10.1. Оформление заказа</strong></p>
<p>Заказ оформляется Покупателем в строгом соответствии с процедурой, установленной на сайте.</p>
<p><strong>10.2. Ответственность за некорректные данные</strong></p>
<p>Покупатель принимает на себя риски неполучения заказа, связанные с ошибочной или неполной информацией, предоставленной при оформлении заказа.</p>
<p><strong>10.3. Предварительная информация о доставке</strong></p>
<p>После формирования заказа Покупатель получает уведомление о примерной дате доставки через электронную почту или телефон, указанный при регистрации.</p>
<p><strong>10.4. Конкретизация времени доставки</strong></p>
<p>Перед доставкой сотрудник магазина свяжется с Покупателем по телефону для уточнения точного времени доставки.</p>
<p><strong>10.5. Уточнение ожидаемого времени доставки</strong></p>
<p>Менеджер по заказу уведомляет Покупателя о планируемой дате передачи заказа в службу доставки через электронную почту или звонок.</p>
<p><strong>10.6. Возможность нехватки товара</strong></p>
<p>Продавец не гарантирует постоянное наличие заказанного товара на складе на момент обработки заказа.</p>
<p><strong>10.11. Законодательные рамки поставок</strong></p>
<p>Поставка товаров осуществляется в соответствии с законами Российской Федерации и страны происхождения Продавца.</p>
<p><strong>10.12. Применимость российского законодательства</strong></p>
<p>Физические товары поступают к покупателю в соответствии с законодательством Российской Федерации.</p>
</section>
<section class="legal-section">
<h2>11. Доставка заказа</h2>
<p><strong>11.1. Способы и сроки доставки</strong></p>
<p>Способы и ориентировочные сроки доставки товаров указаны на сайте. Фактические сроки могут быть уточнены при подтверждении заказа сотрудником call-центра или представителем Продавца.</p>
<p><strong>11.2. Подтверждение поставки цифровых товаров</strong></p>
<p>Факт поставки цифровых товаров подтверждается отправкой электронного письма на указанный при регистрации адрес с приложением цифрового файла или кода. Получение такого письма означает выполнение услуги по поставке цифровых товаров.</p>
<p>Физический товар считается доставленным в момент его реальной передачи Покупателю или назначенному им получателю согласно условиям выбранной доставки.</p>
<p><strong>11.3. Территория доставки</strong></p>
<p>Территориальные границы доставки товаров, предлагаемых Продавцом, опубликованы на сайте.</p>
<p><strong>11.4. Прием товара</strong></p>
<p>При доставке товар передается Покупателю или указанному в заказе третьему лицу.</p>
<p><strong>11.5. Проверка личности</strong></p>
<p>При передаче предварительно оплаченного заказа служба доставки вправе попросить предъявить удостоверение личности для исключения риска мошенничества.</p>
<p><strong>11.6. Переход риска утраты товара</strong></p>
<p>Риск случайной потери или порчи товара переходит к Покупателю в момент непосредственной передачи заказа.</p>
<p><strong>11.7. Расчёт стоимости доставки</strong></p>
<p>Стоимость доставки рассчитывается индивидуально для каждого заказа, учитывая вес, регион и выбранный способ транспортировки.</p>
<p><strong>11.8. Завершение обязательств Продавца</strong></p>
<p>Продавец выполнил свои обязательства по передаче товара, если товар передан Покупателю курьером или выдан Покупателем в почтовом отделении или другом согласованном месте.</p>
<p><strong>11.9. Осмотр товара при получении</strong></p>
<p>При получении товара Покупатель вправе визуально оценить его состояние и при необходимости провести осмотр в присутствии сотрудника Продавца или курьера.</p>
<p><strong>11.10. Проверка товара при приеме от курьера</strong></p>
<p>Покупатель может осмотреть полученный товар и убедиться в его соответствии заявленному количеству, ассортименту и комплектации.</p>
<p><strong>11.11. Лимиты времени для курьерской доставки</strong></p>
<p>Курьеры имеют ограниченное время пребывания по адресу Покупателя — максимум 20 минут.</p>
<p><strong>11.12. Сертификация товара</strong></p>
<p>Товары, размещённые на сайте, соответствуют государственным стандартам (ГОСТ) и техническим условиям (ТУ), что подтверждено сертификатами и прочими необходимыми документами.</p>
</section>
<section class="legal-section">
<h2>12. Оплата товара</h2>
<p><strong>12.1. Валюта и структура цены</strong></p>
<p>Цена товаров, размещённых на сайте, выражается в рублях Российской Федерации и включает все налоговые отчисления и обязательные сборы.</p>
<p><strong>12.2. Возможные ошибки в ценах</strong></p>
<p>Иногда на сайте могут возникать технические ошибки, приводящие к неверному отражению цен на товары или стоимости доставки. При обнаружении таких ошибок Продавец уведомляет Покупателя и уточняет цену, оставляя Покупателю выбор подтвердить заказ по обновлённым расценкам или отменить сделку.</p>
<p><strong>12.3. Изменение цен</strong></p>
<p>Продавец вправе изменять цены на товары в одностороннем порядке. Однако если товар уже оплачен Покупателем, изменение цены на этот товар не допускается.</p>
<p><strong>12.4. Способы оплаты</strong></p>
<p>Варианты оплаты, доступные Покупателю, приведены на сайте в процессе оформления заказа. Метод оплаты принимается на основании выбора Пользователя из числа доступных опций.</p>
<p><strong>12.6. Особенности оплаты с помощью банковских карт</strong></p>
<p>При оплате товаров с использованием банковских карт действуют следующие правила:</p>
<ul>
<li>Платежи осуществляются держателем карты или лицом, имеющим доверенность.</li>
<li>Авторизация операций проходит через банковскую организацию.</li>
<li>Продавец вправе запросить документ, удостоверяющий личность, для подтверждения полномочий на использование карты.</li>
<li>При осуществлении оплаты банковской картой на сайте Покупатель соглашается на получение кассового чека в электронной форме по электронной почте.</li>
<li>Заказы, оплаченные картой, проверяются Продавцом для предотвращения мошеннических действий.</li>
<li>Сбор и хранение данных банковских карт Покупателей не производятся ни Владельцем сайта, ни Продавцом.</li>
</ul>
<p><strong>12.7. Скидки и бонусы</strong></p>
<p>Продавец вправе назначать скидки и вводить бонусные программы. Типы скидок, порядок и условия начисления устанавливаются Продавцом и публикуются на сайте.</p>
<p><strong>12.13. Ограничения по сумме оплаты наличными средствами</strong></p>
<p>Оплата товара наличными средствами не осуществляется.</p>
<p><strong>12.14. Сроки возврата денежных средств</strong></p>
<p>Требования Покупателя о возврате средств за товар удовлетворяются в течение 10 дней с момента представления соответствующего заявления.</p>
</section>
<section class="legal-section">
<h2>13. Возврат и обмен товара</h2>
<p><strong>13.1. Общие правила возврата</strong></p>
<p>Цифровые товары (предоставляемые в электронной форме) не подлежат возврату согласно российскому законодательству. Возврат физических товаров осуществляется в соответствии с разделом <a routerLink="/return-policy">«Политика возврата»</a> и действующими законами о правах потребителей.</p>
<p><strong>13.2. Возврат товара</strong></p>
<p>Возврат или замена товаров, представленных на сайте и подлежащих возврату, происходят в соответствии с данным соглашением и законодательством Российской Федерации.</p>
<p><strong>13.4. Акционные наборы</strong></p>
<p>При приобретении набора товаров по акции с предоставлением скидки возврат или обмен возможен только в комплексе. Отдельные товары из комплекта вернуть нельзя.</p>
<p><strong>13.5. Компенсация затрат на доставку</strong></p>
<p>При возврате качественного товара Продавец или Владелец сайта вправе взыскать с Покупателя затраты на доставку товара.</p>
<p><strong>13.6. Сроки удовлетворения требований</strong></p>
<p>Заявления о возврате средств удовлетворяются в течение 10 дней с момента подачи.</p>
<p><strong>13.6.7. Документация при возврате товара</strong></p>
<p>При возврате товара Покупатель обязан предоставить следующие документы:</p>
<ul>
<li>Заявление на возврат.</li>
<li>Копию квитанции об оплате или чек.</li>
<li>Копию акта приема-передачи (опись вложения).</li>
</ul>
<p><strong>13.6.9. Механизм возврата средств</strong></p>
<p>Возврат средств осуществляется путем возвращения стоимости оплаченного товара. Способ возврата указывается в заявлении на возврат.</p>
</section>
<section class="legal-section">
<h2>14. Срок действия соглашения</h2>
<p><strong>14.1. Начало и прекращение действия</strong></p>
<p>Соглашение становится активным с момента его принятия (акцепта) Пользователем или Покупателем и действует вплоть до момента отзыва акцепта публичной оферты. Отзыв оферты влечет немедленное прекращение действия соглашения.</p>
<p><strong>14.2. Право отзыва оферты</strong></p>
<p>Владелец сайта имеет право отозвать данное предложение в порядке, предусмотренном статьей 436 Гражданского кодекса Российской Федерации.</p>
</section>
<section class="legal-section">
<h2>15. Порядок разрешения споров и урегулирования претензий</h2>
<p><strong>15.1. Добровольное урегулирование споров</strong></p>
<p>В случае возникновения разногласий между Пользователем и Владельцем сайта, связанных с исполнением условий соглашения, обе стороны обязаны попытаться урегулировать проблему путем взаимных консультаций и переговоров. Установлен обязательный досудебный порядок рассмотрения споров.</p>
<p><strong>15.2. Претензионный порядок</strong></p>
<p>Процедура рассмотрения претензий предусматривает следующие этапы:</p>
<ul>
<li>Пользователь, полагающий, что его права были нарушены действием или бездействием Владельца сайта, направляет соответствующую претензию, содержащую описание сути претензии.</li>
<li>В течение 25 рабочих дней с момента получения претензии Владельцем сайта последний обязан подготовить ответ и разъяснить свою позицию.</li>
<li>Если достичь компромисса не удается, спор рассматривается в судебном порядке в соответствии с пунктом 15.5 соглашения.</li>
</ul>
<p><strong>15.3. Рассмотрение претензий</strong></p>
<p>Владельцем сайта не принимаются к рассмотрению анонимные претензии или жалобы, содержащие недостаточно информации для идентификации заявителя.</p>
<p><strong>15.5. Судебное рассмотрение споров</strong></p>
<p>Если переговоры и претензионный порядок не приводят к соглашению, спор решается в судебном порядке по месту нахождения Владельца сайта.</p>
</section>
<section class="legal-section">
<h2>16. Прочие условия</h2>
<p><strong>16.1. Действие соглашения</strong></p>
<p>Данный документ представляет собой официальное соглашение между Пользователем и Владельцем сайта относительно использования ресурса и замещает собой все предшествующие договоренности.</p>
<p><strong>16.2. Согласие с условиями</strong></p>
<p>Пользователь, продолжая использовать сайт, выражает своё согласие с условиями соглашения. Несогласие предполагает удаление личного кабинета.</p>
<p><strong>16.3. Применимое законодательство</strong></p>
<p>Вопросы, не урегулированные соглашением, разрешаются в соответствии с законодательством Армении.</p>
<p><strong>16.4. Определение термина «Законодательство»</strong></p>
<p>Термин «законодательство» по всему тексту соглашения, если специально не указано иначе, подразумевает законы Армении.</p>
<p><strong>16.5. Бесплатные услуги</strong></p>
<p>Бесплатные услуги, предоставляемые в рамках соглашения, не предполагают применение норм законодательства о защите прав потребителей к взаимоотношениям между Пользователем и Продавцом.</p>
<p><strong>16.6. Отсутствующие отношения</strong></p>
<p>Никакие положения соглашения не могут трактоваться как установление агентских связей, партнёрства, совместного предприятия, трудовых отношений или иных видов взаимоотношений, прямо не предусмотренных соглашением.</p>
<p><strong>16.7. Признание недействительности пунктов</strong></p>
<p>Признание одного или нескольких положений соглашения недействительным не влияет на законную силу и применимость оставшихся пунктов.</p>
<p><strong>16.8. Реакция на нарушения</strong></p>
<p>Невмешательство Владельца сайта в случае нарушений соглашений Пользователями не препятствует последующим мерам защиты интересов Владельца позже.</p>
</section>
</div>
</div>

View File

@@ -1,134 +0,0 @@
<div class="legal-page">
<div class="legal-container">
<h1>Политика возврата товаров</h1>
<section class="legal-section">
<h2>1. Общие положения</h2>
<p>1.1. Настоящий документ устанавливает правила возврата товаров и отказ от услуг, приобретенных через маркетплейс DexarMarket.</p>
<p>1.2. Маркетплейс выполняет лишь роль информационного посредника, и все вопросы возврата регулируются между Покупателем и Продавцом в соответствии с нормами российского законодательства.</p>
<p>1.3. Возврат товаров надлежащего качества регламентируется статьей 26.1 Закона РФ «О защите прав потребителей».</p>
<p>1.4. Возврат некачественных товаров подчиняется статьям 1824 Закона РФ «О защите прав потребителей».</p>
<p>1.5. Особенности возврата цифровых товаров определены российским законодательством.</p>
</section>
<section class="legal-section">
<h2>2. Сроки возврата</h2>
<p><strong>2.1. Товары надлежащего качества:</strong></p>
<ul>
<li><strong>Дистанционный способ приобретения:</strong> 7 дней с момента получения товара.</li>
<li><strong>До передачи товара:</strong> в любое время.</li>
<li><strong>Без письменного извещения о правилах возврата:</strong> 3 месяца с момента получения.</li>
</ul>
<p><strong>2.2. Товары ненадлежащего качества:</strong></p>
<ul>
<li>Внутри установленного гарантийного срока.</li>
<li>Если гарантия не указана: 2 года с момента передачи товара.</li>
<li>Для существенных дефектов: весь срок службы товара.</li>
</ul>
<p><strong>2.3. Цифровые товары:</strong></p>
<p>Возврат доступен только до начала скачивания или активации продукта. После начала использования возврат невозможен (за исключением случаев наличия значительных дефектов, мешающих нормальному применению товара).</p>
</section>
<section class="legal-section">
<h2>3. Условия возврата товаров надлежащего качества</h2>
<p>Вернуть товар можно при выполнении следующих требований:</p>
<ul>
<li>Сохранён внешний вид и потребительские характеристики товара.</li>
<li>Полностью сохранена оригинальная упаковка и вся комплектация.</li>
<li>Сохранены все заводские ярлыки, пломбы и наклейки.</li>
<li>Товар не использовался и не имеет следов износа.</li>
<li>Имеются доказательства покупки (чек, квитанция и т.д.).</li>
</ul>
<p>Покупатель оплачивает расходы на доставку при возврате качественного товара, если иное не оговорено отдельно с Продавцом.</p>
<p>Продавец обязан возместить деньги за качественный товар в течение 10 дней после подачи заявления о возврате.</p>
</section>
<section class="legal-section">
<h2>4. Категории товаров, не подлежащих возврату</h2>
<p>Согласно Постановлению Правительства РФ №2463 от 31 декабря 2020 г., нижеперечисленные группы товаров надлежащего качества не подлежат возврату:</p>
<ul>
<li>Медицинские товары для домашнего пользования.</li>
<li>Предметы индивидуальной гигиены (расчёски, зубные щётки и т.д.).</li>
<li>Парфюмерия и косметика.</li>
<li>Метражные ткани, нити, ленты и аналогичные продукты.</li>
<li>Бельевые изделия, нижнее бельё, колготки.</li>
<li>Посуда и столовые принадлежности одноразового использования.</li>
<li>Бытовая химия, удобрения и химикаты.</li>
<li>Мебель.</li>
<li>Украшения и ювелирные изделия.</li>
<li>Автотранспортные средства и запчасти.</li>
<li>Сложнотехнические бытовые товары, имеющие гарантию.</li>
<li>Гражданское оружие и боеприпасы.</li>
<li>Животные и растения.</li>
<li>Периодически не выпускающиеся издания (книги, журналы, альбомы).</li>
</ul>
<p>Данные товары допускаются к возврату только при выявлении брака или дефекта.</p>
</section>
<section class="legal-section">
<h2>5. Процедура возврата</h2>
<p>Алгоритм возврата выглядит следующим образом:</p>
<ol>
<li>Связаться с Продавцом через сайт или предоставленный контакт.</li>
<li>Сообщить причину возврата и представить доказательства неисправности (фотографии, видеозаписи).</li>
<li>Получить инструкцию по отправке товара назад.</li>
<li>Упаковать товар в оригинальную коробку вместе с аксессуарами.</li>
<li>Отправить товар установленным методом.</li>
<li>Приложить почтовый трек-номер Продавцу.</li>
<li>Ждать проверки товара Продавцом (до 10 дней).</li>
<li>Деньги будут возвращены Покупателю.</li>
</ol>
<p>Маркетплейс готов поддержать разрешение конфликта, если такая необходимость возникнет.</p>
</section>
<section class="legal-section">
<h2>6. Возврат денежных средств</h2>
<p>Денежные средства возвращаются таким же способом, каким производилась оплата:</p>
<ul>
<li>Через банковскую карту.</li>
<li>Электронный кошелёк.</li>
<li>По ссылке на оплату.</li>
</ul>
<p><strong>Срок возврата:</strong></p>
<ul>
<li><strong>Качественный товар:</strong> 10 дней с момента принятия Продавцом заявления.</li>
<li><strong>Некачественный товар:</strong> 10 дней с момента обращения Покупателя.</li>
</ul>
<p>Покупатель вправе требовать возмещения стоимости дефектного товара и компенсации расходов на доставку.</p>
</section>
<section class="legal-section">
<h2>7. Обмен товара</h2>
<p>Если товар надлежащего качества не подходит по форме, размерам или характеристикам, Покупатель может обменять его на аналогичный продукт у Продавца.</p>
<p>Если подходящего товара нет в наличии, Покупатель вправе отказаться от покупки и вернуть потраченные средства.</p>
</section>
<section class="legal-section">
<h2>8. Гарантийное обслуживание</h2>
<p>Информация о гарантийных обязательствах доступна в соответствующем разделе каталога или у Продавца.</p>
<p>Гарантия выполняется самим Продавцом или официальным сервисом производителя.</p>
<p>Гарантийный случай не действует при механических повреждениях, неправильном использовании или самостоятельном ремонте.</p>
</section>
<section class="legal-section">
<h2>9. Ответственность сторон</h2>
<p>Продавец отвечает за качество товара и своевременность возврата средств.</p>
<p>Покупатель обязан сохранять целостность упаковки и комплектность товара до момента возврата.</p>
<p>Маркетплейс, выступая посредником, не несёт ответственности за невыполнение Продавцом обязательств, но помогает разрешать конфликты.</p>
</section>
<section class="legal-section">
<h2>10. Контактная информация</h2>
<p>Вопросы по возврату товаров можно задать Продавцу напрямую, контактные данные предоставляются в заказе.</p>
<p>Обращаясь к администрации Маркетплейса для разрешения разногласий:</p>
<ul>
<li><strong>Email Маркетплейса:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></li>
<li><strong>Тема письма:</strong> «Спор с Продавцом — №Заказа»</li>
<li><strong>Необходимые файлы:</strong> копии писем, фотографии товара, чек об оплате.</li>
</ul>
<p>Если конфликт невозможно разрешить мирно, Покупатель вправе подать жалобу в Роспотребнадзор или суд по месту расположения Продавца.</p>
</section>
</div>
</div>

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-about-novo', selector: 'app-about-novo',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './about.component.html', templateUrl: './about.component.html',
styleUrls: ['../../../../../pages/info/about/about.component.scss'] styleUrls: ['../../../../../pages/info/about/about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AboutNovoComponent {} export class AboutNovoComponent {}

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-contacts-novo', selector: 'app-contacts-novo',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './contacts.component.html', templateUrl: './contacts.component.html',
styleUrls: ['../../../../../pages/info/contacts/contacts.component.scss'] styleUrls: ['../../../../../pages/info/contacts/contacts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ContactsNovoComponent {} export class ContactsNovoComponent {}

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-delivery-novo', selector: 'app-delivery-novo',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './delivery.component.html', templateUrl: './delivery.component.html',
styleUrls: ['../../../../../pages/info/delivery/delivery.component.scss'] styleUrls: ['../../../../../pages/info/delivery/delivery.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DeliveryNovoComponent {} export class DeliveryNovoComponent {}

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-faq-novo', selector: 'app-faq-novo',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './faq.component.html', templateUrl: './faq.component.html',
styleUrls: ['../../../../../pages/info/faq/faq.component.scss'] styleUrls: ['../../../../../pages/info/faq/faq.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class FaqNovoComponent {} export class FaqNovoComponent {}

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-guarantee-novo', selector: 'app-guarantee-novo',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './guarantee.component.html', templateUrl: './guarantee.component.html',
styleUrls: ['../../../../../pages/info/guarantee/guarantee.component.scss'] styleUrls: ['../../../../../pages/info/guarantee/guarantee.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class GuaranteeNovoComponent {} export class GuaranteeNovoComponent {}

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-company-details-novo', selector: 'app-company-details-novo',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './company-details.component.html', templateUrl: './company-details.component.html',
styleUrls: ['../../../../../pages/legal/company-details/company-details.component.scss'] styleUrls: ['../../../../../pages/legal/company-details/company-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class CompanyDetailsNovoComponent {} export class CompanyDetailsNovoComponent {}

View File

@@ -103,7 +103,7 @@
<section class="info-card wide"> <section class="info-card wide">
<div class="card-icon">↩️</div> <div class="card-icon">↩️</div>
<h2>6. Возврат средств</h2> <h2>6. Возврат средств</h2>
<p>6.1. Порядок возврата денежных средств регулируется <a routerLink="/return-policy">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p> <p>6.1. Порядок возврата денежных средств регулируется <a [routerLink]="'/return-policy' | langRoute">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p>
<p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p> <p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p>
<p>6.3. Срок возврата денежных средств составляет:</p> <p>6.3. Срок возврата денежных средств составляет:</p>
<div class="refund-times"> <div class="refund-times">

View File

@@ -1,11 +1,12 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router';
import { LangRoutePipe } from '../../../../../pipes/lang-route.pipe';
@Component({ @Component({
selector: 'app-payment-terms-novo', selector: 'app-payment-terms-novo',
standalone: true, imports: [RouterLink, LangRoutePipe],
imports: [CommonModule],
templateUrl: './payment-terms.component.html', templateUrl: './payment-terms.component.html',
styleUrls: ['../../../../../pages/legal/payment-terms/payment-terms.component.scss'] styleUrls: ['../../../../../pages/legal/payment-terms/payment-terms.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PaymentTermsNovoComponent {} export class PaymentTermsNovoComponent {}

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-privacy-policy-novo', selector: 'app-privacy-policy-novo',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './privacy-policy.component.html', templateUrl: './privacy-policy.component.html',
styleUrls: ['../../../../../pages/legal/privacy-policy/privacy-policy.component.scss'] styleUrls: ['../../../../../pages/legal/privacy-policy/privacy-policy.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PrivacyPolicyNovoComponent {} export class PrivacyPolicyNovoComponent {}

View File

@@ -31,7 +31,7 @@
<p>1.4. Акцепт происходит автоматически при любом действии: визите, регистрации, оформлении покупки.</p> <p>1.4. Акцепт происходит автоматически при любом действии: визите, регистрации, оформлении покупки.</p>
<p>1.5. Подписание бумажного договора не требуется — электронная форма юридически действительна.</p> <p>1.5. Подписание бумажного договора не требуется — электронная форма юридически действительна.</p>
<p>1.6. Несогласие с условиями означает обязанность покинуть сайт.</p> <p>1.6. Несогласие с условиями означает обязанность покинуть сайт.</p>
<p>1.7. Также применяется <a routerLink="/privacy-policy">Политика конфиденциальности</a>.</p> <p>1.7. Также применяется <a [routerLink]="'/privacy-policy' | langRoute">Политика конфиденциальности</a>.</p>
<p>1.8. Мы можем обновлять условия в одностороннем порядке.</p> <p>1.8. Мы можем обновлять условия в одностороннем порядке.</p>
<p>1.9. Промо-кампании могут иметь специальные правила.</p> <p>1.9. Промо-кампании могут иметь специальные правила.</p>
</section> </section>
@@ -75,7 +75,7 @@
<ul class="compact-list"> <ul class="compact-list">
<li>Устанавливать ограничения в использовании ресурса</li> <li>Устанавливать ограничения в использовании ресурса</li>
<li>Направлять информацию о нововведениях</li> <li>Направлять информацию о нововведениях</li>
<li>Передавать полномочия третьим лицам</li> <li>Изменять поставщика товаров и/или компанию-доставщика без предварительного согласия Покупателя</li>
<li>Изменять условия акций в одностороннем порядке</li> <li>Изменять условия акций в одностороннем порядке</li>
<li>Ограничивать действия пользователей, создающих риски</li> <li>Ограничивать действия пользователей, создающих риски</li>
<li>Проводить технические работы без предупреждения</li> <li>Проводить технические работы без предупреждения</li>
@@ -134,17 +134,15 @@
<section class="info-card wide"> <section class="info-card wide">
<div class="card-icon">⚖️</div> <div class="card-icon">⚖️</div>
<h2>9. Ответственность сторон</h2> <h2>9. Ответственность сторон</h2>
<p><strong>9.1. Общее:</strong> Пользователь использует сайт на свой риск. Ресурсы предоставляются "как есть".</p> <p><strong>9.1. Общее:</strong> Сайт является информационно-технологическим ресурсом, обеспечивающим размещение информации о товарах и услугах, а также безопасность совершения сделок. Владелец сайта принимает на себя ответственность за недопущение размещения запрещённых товаров на ресурсе, а также за качество и достоверность информации о товарах и услугах, представленных на платформе.</p>
<p><strong>9.2. Ограничения:</strong> Владелец не гарантирует постоянную доступность и отсутствие ошибок.</p> <p><strong>9.2. Ограничения:</strong> Владелец не гарантирует постоянную доступность и отсутствие ошибок.</p>
<p><strong>9.3. Информация:</strong> Пользователь несёт ответственность за последствия использования информации.</p> <p><strong>9.3. Информация:</strong> Пользователь несёт ответственность за последствия использования информации.</p>
<p><strong>9.4. Недостоверные данные:</strong> Владелец не отвечает за неверные данные пользователя.</p> <p><strong>9.4. Идентификация и контроль:</strong> Покупатель идентифицируется по номеру телефона и данным, предоставляемым через Telegram. Каждый Продавец проходит полную процедуру идентификации (онбординг), его данные доступны при возникновении спорных ситуаций. Владелец сайта осуществляет проверку и контроль деятельности Продавцов на платформе.</p>
<p><strong>9.5. Безопасность аккаунта:</strong> Пользователь отвечает за сохранность логина и пароля.</p> <p><strong>9.5. Безопасность аккаунта:</strong> Пользователь отвечает за сохранность логина и пароля.</p>
<p><strong>9.6. Ответственность продавца:</strong> Продавец отвечает за качество, безопасность и соответствие товаров.</p> <p><strong>9.6. Ответственность продавца:</strong> Продавец отвечает за качество, безопасность и соответствие товаров.</p>
<p><strong>9.7. Освобождение владельца:</strong> Владелец не отвечает за:</p> <p><strong>9.7. Ответственность владельца:</strong> Владелец несёт ответственность за качество, безопасность и достоверность информации о товарах и услугах на платформе. При этом Владелец не отвечает за:</p>
<ul class="compact-list"> <ul class="compact-list">
<li>Качество, безопасность и правомерность товаров продавцов</li>
<li>Выполнение продавцами обязательств</li> <li>Выполнение продавцами обязательств</li>
<li>Истинность информации о товарах</li>
<li>Нарушение прав третьих лиц</li> <li>Нарушение прав третьих лиц</li>
<li>Доставку, комплектацию и состояние товаров</li> <li>Доставку, комплектацию и состояние товаров</li>
</ul> </ul>
@@ -206,7 +204,7 @@
<section class="info-card wide"> <section class="info-card wide">
<div class="card-icon">↩️</div> <div class="card-icon">↩️</div>
<h2>13. Возврат и обмен товара</h2> <h2>13. Возврат и обмен товара</h2>
<p><strong>13.1. Общие правила:</strong> Цифровые товары не подлежат возврату. Физические товары — согласно <a routerLink="/return-policy">Политике возврата</a> и законам о правах потребителей.</p> <p><strong>13.1. Общие правила:</strong> Цифровые товары не подлежат возврату. Физические товары — согласно <a [routerLink]="'/return-policy' | langRoute">Политике возврата</a> и законам о правах потребителей.</p>
<p><strong>13.2. Процедура возврата:</strong> В соответствии с соглашением и законодательством РФ.</p> <p><strong>13.2. Процедура возврата:</strong> В соответствии с соглашением и законодательством РФ.</p>
<p><strong>13.3. Акционные наборы:</strong> Возврат только в комплексе, отдельные товары вернуть нельзя.</p> <p><strong>13.3. Акционные наборы:</strong> Возврат только в комплексе, отдельные товары вернуть нельзя.</p>
<p><strong>13.4. Затраты на доставку:</strong> При возврате качественного товара продавец может взыскать затраты на доставку.</p> <p><strong>13.4. Затраты на доставку:</strong> При возврате качественного товара продавец может взыскать затраты на доставку.</p>

View File

@@ -1,11 +1,12 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router';
import { LangRoutePipe } from '../../../../../pipes/lang-route.pipe';
@Component({ @Component({
selector: 'app-public-offer-novo', selector: 'app-public-offer-novo',
standalone: true, imports: [RouterLink, LangRoutePipe],
imports: [CommonModule],
templateUrl: './public-offer.component.html', templateUrl: './public-offer.component.html',
styleUrls: ['../../../../../pages/legal/public-offer/public-offer.component.scss'] styleUrls: ['../../../../../pages/legal/public-offer/public-offer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PublicOfferNovoComponent {} export class PublicOfferNovoComponent {}

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-return-policy-novo', selector: 'app-return-policy-novo',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './return-policy.component.html', templateUrl: './return-policy.component.html',
styleUrls: ['../../../../../pages/legal/return-policy/return-policy.component.scss'] styleUrls: ['../../../../../pages/legal/return-policy/return-policy.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReturnPolicyNovoComponent {} export class ReturnPolicyNovoComponent {}

View File

@@ -0,0 +1,71 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { Location } from '@angular/common';
import { environment } from '../../../environments/environment';
import { TranslateService } from '../../i18n/translate.service';
@Component({
selector: 'app-back-button',
template: `
@if (!isnovo) {
<button class="dexar-back-btn" (click)="goBack()" [attr.aria-label]="i18n.t('itemDetail.back')">
<svg width="37" height="24" viewBox="0 0 37 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.73 11.46c-.97-.52-.97-1.4 0-1.92L20.39 1.21c1.48-.79 3.89-.19 3.89.95v3.74h6.94c1.28 0 2.31.59 2.31 1.31v6.56c0 .73-1.03 1.31-2.31 1.31h-6.94v3.74c0 1.15-2.42 1.74-3.89.96L4.73 11.46Z"
fill="#497671" fill-opacity="0.75" stroke="white" stroke-opacity="0.6" stroke-linejoin="round"/>
</svg>
</button>
}
`,
styles: [`
.dexar-back-btn {
position: fixed;
top: 76px;
left: 20px;
z-index: 100;
background: none;
border: none;
cursor: pointer;
padding: 4px;
transition: transform 0.2s ease;
svg path {
transition: fill 0.2s ease, fill-opacity 0.2s ease;
}
&:hover {
transform: scale(1.08);
svg path {
fill: #A1B4B5;
fill-opacity: 1;
}
}
&:active {
transform: scale(0.95);
}
}
@media (max-width: 768px) {
.dexar-back-btn {
top: 68px;
left: 12px;
svg {
width: 30px;
height: 20px;
}
}
}
`],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BackButtonComponent {
isnovo = environment.theme === 'novo';
i18n = inject(TranslateService);
constructor(private location: Location) {}
goBack(): void {
this.location.back();
}
}

View File

@@ -1,9 +1,9 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({ @Component({
selector: 'app-empty-cart-icon', selector: 'app-empty-cart-icon',
standalone: true,
templateUrl: './empty-cart-icon.component.html', templateUrl: './empty-cart-icon.component.html',
styleUrls: ['./empty-cart-icon.component.scss'] styleUrls: ['./empty-cart-icon.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class EmptyCartIconComponent {} export class EmptyCartIconComponent {}

View File

@@ -5,33 +5,33 @@
<div class="novo-footer-top"> <div class="novo-footer-top">
<div class="novo-footer-col"> <div class="novo-footer-col">
<h4>{{ brandName }}</h4> <h4>{{ brandName }}</h4>
<p class="novo-footer-desc">Современный маркетплейс для комфортных покупок</p> <p class="novo-footer-desc">{{ 'footer.description' | translate }}</p>
</div> </div>
<div class="novo-footer-col"> <div class="novo-footer-col">
<h4>Компания</h4> <h4>{{ 'footer.company' | translate }}</h4>
<ul class="novo-footer-links"> <ul class="novo-footer-links">
<li><a routerLink="/about">О нас</a></li> <li><a [routerLink]="'/about' | langRoute">{{ 'footer.aboutUs' | translate }}</a></li>
<li><a routerLink="/contacts">Контакты</a></li> <li><a [routerLink]="'/contacts' | langRoute">{{ 'footer.contacts' | translate }}</a></li>
<li><a routerLink="/company-details">Реквизиты</a></li> <li><a [routerLink]="'/company-details' | langRoute">{{ 'footer.requisites' | translate }}</a></li>
</ul> </ul>
</div> </div>
<div class="novo-footer-col"> <div class="novo-footer-col">
<h4>Поддержка</h4> <h4>{{ 'footer.support' | translate }}</h4>
<ul class="novo-footer-links"> <ul class="novo-footer-links">
<li><a routerLink="/faq">FAQ</a></li> <li><a [routerLink]="'/faq' | langRoute">{{ 'footer.faq' | translate }}</a></li>
<li><a routerLink="/delivery">Доставка</a></li> <li><a [routerLink]="'/delivery' | langRoute">{{ 'footer.delivery' | translate }}</a></li>
<li><a routerLink="/guarantee">Гарантия</a></li> <li><a [routerLink]="'/guarantee' | langRoute">{{ 'footer.guarantee' | translate }}</a></li>
</ul> </ul>
</div> </div>
<div class="novo-footer-col"> <div class="novo-footer-col">
<h4>Правовая информация</h4> <h4>{{ 'footer.legal' | translate }}</h4>
<ul class="novo-footer-links"> <ul class="novo-footer-links">
<li><a routerLink="/public-offer">Оферта</a></li> <li><a [routerLink]="'/public-offer' | langRoute">{{ 'footer.offer' | translate }}</a></li>
<li><a routerLink="/privacy-policy">Конфиденциальность</a></li> <li><a [routerLink]="'/privacy-policy' | langRoute">{{ 'footer.privacy' | translate }}</a></li>
<li><a routerLink="/return-policy">Возврат</a></li> <li><a [routerLink]="'/return-policy' | langRoute">{{ 'footer.returns' | translate }}</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -49,49 +49,60 @@
</div> </div>
</footer> </footer>
} @else { } @else {
<!-- DEXAR VERSION - Original --> <!-- DEXAR VERSION - Redesigned -->
<footer class="footer"> <footer class="dexar-footer">
<div class="footer-container"> <div class="dexar-footer-bg">
<div class="footer-section"> <div class="dexar-footer-container">
<h3>Информация</h3> <div class="dexar-footer-top">
<div class="dexar-footer-logo">
<img src="/assets/images/dexar-logo.svg" alt="Dexar" class="dexar-footer-logo-desktop" loading="lazy" />
<img src="/assets/images/dexar-logo-small.svg" alt="Dexar" class="dexar-footer-logo-mobile" loading="lazy" />
</div>
<div class="dexar-footer-columns">
<div class="dexar-footer-col">
<h4>{{ 'footer.info' | translate }}</h4>
<ul> <ul>
<li><a routerLink="/about">О компании</a></li> <li><a [routerLink]="'/about' | langRoute">{{ 'footer.aboutCompany' | translate }}</a></li>
<li><a routerLink="/contacts">Контакты</a></li> <li><a [routerLink]="'/contacts' | langRoute">{{ 'footer.contacts' | translate }}</a></li>
<li><a routerLink="/company-details">Реквизиты организации</a></li> <li><a [routerLink]="'/company-details' | langRoute">{{ 'footer.requisites' | translate }}</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="dexar-footer-col">
<h3>Документы</h3> <h4>{{ 'footer.documents' | translate }}</h4>
<ul> <ul>
<li><a routerLink="/payment-terms">Правила оплаты</a></li> <li><a [routerLink]="'/payment-terms' | langRoute">{{ 'footer.paymentRules' | translate }}</a></li>
<li><a routerLink="/return-policy">Политика возврата</a></li> <li><a [routerLink]="'/return-policy' | langRoute">{{ 'footer.returnPolicy' | translate }}</a></li>
<li><a routerLink="/public-offer">Публичная оферта</a></li> <li><a [routerLink]="'/public-offer' | langRoute">{{ 'footer.publicOffer' | translate }}</a></li>
<li><a routerLink="/privacy-policy">Политика конфиденциальности</a></li> <li><a [routerLink]="'/privacy-policy' | langRoute">{{ 'footer.privacy' | translate }}</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="dexar-footer-col">
<h3>Помощь</h3> <h4>{{ 'footer.help' | translate }}</h4>
<ul> <ul>
<li><a routerLink="/faq">Часто задаваемые вопросы</a></li> <li><a [routerLink]="'/faq' | langRoute">{{ 'footer.faq' | translate }}</a></li>
<li><a routerLink="/delivery">Доставка</a></li> <li><a [routerLink]="'/delivery' | langRoute">{{ 'footer.delivery' | translate }}</a></li>
<li><a routerLink="/guarantee">Гарантия</a></li> <li><a [routerLink]="'/guarantee' | langRoute">{{ 'footer.guarantee' | translate }}</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section payment-systems"> <div class="dexar-footer-col">
<h3>Способы оплаты</h3> <h4>{{ 'footer.payment' | translate }}</h4>
<div class="payment-logos"> <div class="dexar-payment-logos">
<img src="/assets/images/mir-logo.svg" alt="МИР" class="payment-logo" loading="lazy" decoding="async" width="60" height="40" /> <img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="48" height="32" />
<img src="/assets/images/visa-logo.svg" alt="Visa" class="payment-logo" loading="lazy" decoding="async" width="60" height="40" /> <img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="48" height="32" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" class="payment-logo" loading="lazy" decoding="async" width="60" height="40" /> <img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="48" height="32" />
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="footer-bottom"> <div class="dexar-footer-bottom">
<p>&copy; {{ currentYear }} {{ brandName }}. Все права защищены.</p> <p>&copy; {{ currentYear }} {{ brandName }}. {{ 'footer.allRightsReserved' | translate }}</p>
</div>
</div>
</div> </div>
</footer> </footer>
} }

View File

@@ -1,4 +1,9 @@
.footer { :host {
display: block;
width: 100%;
}
.footer {
background-color: #1a1a1a; background-color: #1a1a1a;
color: #ffffff; color: #ffffff;
margin-top: auto; margin-top: auto;
@@ -217,3 +222,196 @@
text-align: center; text-align: center;
} }
} }
// ========== DEXAR FOOTER STYLES ==========
.dexar-footer {
margin-top: auto;
}
.dexar-footer-bg {
background-image: url('/assets/images/footer_bg.webp');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
inset: 0;
background: rgba(30, 60, 56, 0.85);
}
}
.dexar-footer-container {
position: relative;
z-index: 1;
max-width: 1440px;
margin: 0 auto;
padding: 48px 40px 24px;
}
.dexar-footer-top {
display: flex;
gap: 48px;
padding-bottom: 32px;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
.dexar-footer-logo {
flex-shrink: 0;
display: flex;
align-items: flex-start;
padding-top: 4px;
.dexar-footer-logo-desktop {
display: block;
height: 40px;
width: auto;
filter: brightness(0) invert(1);
}
.dexar-footer-logo-mobile {
display: none;
height: 32px;
width: auto;
filter: brightness(0) invert(1);
}
}
.dexar-footer-columns {
flex: 1;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 32px;
}
.dexar-footer-col {
h4 {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 15px;
font-weight: 600;
color: #ffffff;
margin: 0 0 16px 0;
}
ul {
list-style: none;
padding: 0;
margin: 0;
li {
margin-bottom: 10px;
a {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 13px;
font-weight: 400;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: color 0.2s ease;
&:hover {
color: #ffffff;
}
}
}
}
}
.dexar-payment-logos {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
img {
height: 28px;
max-height: 28px;
width: auto;
max-width: 60px;
background: rgba(255, 255, 255, 0.9);
padding: 4px 8px;
border-radius: 4px;
transition: opacity 0.2s;
object-fit: contain;
&:hover {
opacity: 0.85;
}
}
}
.dexar-footer-bottom {
padding: 20px 0 0;
text-align: center;
p {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
margin: 0;
}
}
// Responsive
@media (max-width: 992px) {
.dexar-footer-container {
padding: 40px 32px 20px;
}
.dexar-footer-top {
flex-direction: column;
gap: 32px;
}
.dexar-footer-columns {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.dexar-footer-bg {
background-image: url('/assets/images/footer_bg_mobile.webp');
}
.dexar-footer-container {
padding: 32px 20px 16px;
}
.dexar-footer-logo {
.dexar-footer-logo-desktop {
display: none;
}
.dexar-footer-logo-mobile {
display: block;
}
}
.dexar-footer-columns {
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.dexar-footer-col h4 {
font-size: 14px;
margin-bottom: 12px;
}
.dexar-footer-col ul li a {
font-size: 12px;
}
}
@media (max-width: 480px) {
.dexar-footer-container {
padding: 24px 16px 12px;
}
.dexar-footer-columns {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
}

View File

@@ -1,14 +1,15 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
@Component({ @Component({
selector: 'app-footer', selector: 'app-footer',
standalone: true, imports: [RouterLink, LangRoutePipe, TranslatePipe],
imports: [CommonModule, RouterLink],
templateUrl: './footer.component.html', templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss'] styleUrls: ['./footer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class FooterComponent { export class FooterComponent {
currentYear = new Date().getFullYear(); currentYear = new Date().getFullYear();

View File

@@ -3,7 +3,7 @@
<header class="novo-header"> <header class="novo-header">
<div class="novo-header-container"> <div class="novo-header-container">
<div class="novo-left"> <div class="novo-left">
<a routerLink="/" class="novo-logo" (click)="closeMenu()"> <a [routerLink]="'/' | langRoute" class="novo-logo" (click)="closeMenu()">
<app-logo /> <app-logo />
<!-- <span class="novo-brand">{{ brandName }}</span> --> <!-- <span class="novo-brand">{{ brandName }}</span> -->
</a> </a>
@@ -11,17 +11,17 @@
<nav class="novo-nav" [class.novo-nav-open]="menuOpen"> <nav class="novo-nav" [class.novo-nav-open]="menuOpen">
<div class="novo-nav-links"> <div class="novo-nav-links">
<a routerLink="/" routerLinkActive="novo-active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()" class="novo-link"> <a [routerLink]="'/' | langRoute" routerLinkActive="novo-active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()" class="novo-link">
Главная {{ 'header.home' | translate }}
</a> </a>
<a routerLink="/search" routerLinkActive="novo-active" (click)="closeMenu()" class="novo-link"> <a [routerLink]="'/search' | langRoute" routerLinkActive="novo-active" (click)="closeMenu()" class="novo-link">
Поиск {{ 'header.search' | translate }}
</a> </a>
<a routerLink="/about" (click)="closeMenu()" class="novo-link"> <a [routerLink]="'/about' | langRoute" routerLinkActive="novo-active" (click)="closeMenu()" class="novo-link">
О нас {{ 'header.about' | translate }}
</a> </a>
<a routerLink="/contacts" (click)="closeMenu()" class="novo-link"> <a [routerLink]="'/contacts' | langRoute" routerLinkActive="novo-active" (click)="closeMenu()" class="novo-link">
Контакты {{ 'header.contacts' | translate }}
</a> </a>
</div> </div>
</nav> </nav>
@@ -29,7 +29,7 @@
<div class="novo-right"> <div class="novo-right">
<app-language-selector /> <app-language-selector />
<a routerLink="/cart" routerLinkActive="novo-cart-active" class="novo-cart" (click)="closeMenu()"> <a [routerLink]="'/cart' | langRoute" routerLinkActive="novo-cart-active" class="novo-cart" (click)="closeMenu()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="21" r="1"></circle> <circle cx="9" cy="21" r="1"></circle>
<circle cx="20" cy="21" r="1"></circle> <circle cx="20" cy="21" r="1"></circle>
@@ -49,48 +49,131 @@
</div> </div>
</header> </header>
} @else { } @else {
<!-- DEXAR VERSION - Original --> <!-- DEXAR VERSION - Redesigned 2026 -->
<header class="header"> <header class="dexar-header">
<div class="header-container"> <div class="dexar-header-container">
<a routerLink="/" class="logo" (click)="closeMenu()"> <!-- Logo -->
<a [routerLink]="'/' | langRoute" class="dexar-logo" (click)="closeMenu()">
<app-logo /> <app-logo />
<!-- <span class="logo-text">{{ brandName }}</span> -->
</a> </a>
<nav class="nav" [class.nav-open]="menuOpen"> <!-- Navigation Buttons (desktop) -->
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()"> <nav class="dexar-nav">
<span class="nav-icon">🏠</span> <div class="dexar-nav-group">
<span class="nav-text">Главная</span> <a [routerLink]="'/' | langRoute" routerLinkActive="dexar-active" [routerLinkActiveOptions]="{exact: true}"
(click)="closeMenu()" class="dexar-nav-btn dexar-nav-btn-left">
{{ 'header.home' | translate }}
</a> </a>
<a routerLink="/search" routerLinkActive="active" (click)="closeMenu()"> <a [routerLink]="'/about' | langRoute" routerLinkActive="dexar-active" (click)="closeMenu()" class="dexar-nav-btn dexar-nav-btn-middle">
<span class="nav-icon">🔍</span> {{ 'header.about' | translate }}
<span class="nav-text">Поиск</span>
</a> </a>
<a routerLink="/cart" routerLinkActive="active" class="cart-link" (click)="closeMenu()"> <a [routerLink]="'/contacts' | langRoute" routerLinkActive="dexar-active" (click)="closeMenu()" class="dexar-nav-btn dexar-nav-btn-right">
<span class="nav-icon">🛒</span> {{ 'header.contacts' | translate }}
<span class="nav-text">Корзина</span>
@if (cartItemCount() > 0) {
<span class="cart-badge">{{ cartItemCount() }}</span>
}
</a> </a>
</div>
</nav> </nav>
<a routerLink="/cart" routerLinkActive="active" class="cart-link-mobile" (click)="closeMenu()"> <!-- Search Box (desktop) -->
<span class="cart-icon">🛒</span> <div class="dexar-search-wrapper">
<div class="dexar-search-box">
<svg class="dexar-search-icon" width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" fill="#576463" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#576463" />
</svg>
<input type="text" [placeholder]="'header.searchPlaceholder' | translate" class="dexar-search-input" (click)="navigateToSearch()" readonly />
</div>
</div>
<!-- Search Icon (mobile only) -->
<button class="dexar-search-mobile" (click)="navigateToSearch()" [attr.aria-label]="'header.search' | translate">
<svg width="22" height="22" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" fill="#1e3c38" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#1e3c38" />
</svg>
</button>
<!-- Right Actions -->
<div class="dexar-actions">
<!-- Cart Button -->
<a [routerLink]="'/cart' | langRoute" routerLinkActive="dexar-cart-active" class="dexar-cart-btn" (click)="closeMenu()">
<svg width="32" height="24" viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0.5H36C42.3513 0.5 47.5 5.64873 47.5 12V20C47.5 26.3513 42.3513 31.5 36 31.5H12C5.64873 31.5 0.5 26.3513 0.5 20V12C0.5 5.64873 5.64873 0.5 12 0.5Z" fill="transparent" />
<path d="M12 0.5H36C42.3513 0.5 47.5 5.64873 47.5 12V20C47.5 26.3513 42.3513 31.5 36 31.5H12C5.64873 31.5 0.5 26.3513 0.5 20V12C0.5 5.64873 5.64873 0.5 12 0.5Z" stroke="#677B78" />
<path d="M10 3.9C10 3.40294 10.4029 3 10.9 3H13.6C14.013 3 14.373 3.28107 14.4731 3.68172L15.2027 6.6H36.1C36.3677 6.6 36.6216 6.7192 36.7925 6.92523C36.9635 7.13125 37.0339 7.40271 36.9846 7.66586L34.2846 22.0659C34.2048 22.4915 33.8331 22.8 33.4 22.8H31.6H19H17.2C16.7669 22.8 16.3952 22.4915 16.3154 22.0659L13.6204 7.69224L12.8973 4.8H10.9C10.4029 4.8 10 4.39706 10 3.9ZM15.5844 8.4L17.9469 21H32.6531L35.0156 8.4H15.5844ZM19 22.8C17.0118 22.8 15.4 24.4118 15.4 26.4C15.4 28.3882 17.0118 30 19 30C20.9882 30 22.6 28.3882 22.6 26.4C22.6 24.4118 20.9882 22.8 19 22.8ZM31.6 22.8C29.6118 22.8 28 24.4118 28 26.4C28 28.3882 29.6118 30 31.6 30C33.5882 30 35.2 28.3882 35.2 26.4C35.2 24.4118 33.5882 22.8 31.6 22.8ZM19 24.6C19.9941 24.6 20.8 25.4059 20.8 26.4C20.8 27.3941 19.9941 28.2 19 28.2C18.0059 28.2 17.2 27.3941 17.2 26.4C17.2 25.4059 18.0059 24.6 19 24.6ZM31.6 24.6C32.5941 24.6 33.4 25.4059 33.4 26.4C33.4 27.3941 32.5941 28.2 31.6 28.2C30.6059 28.2 29.8 27.3941 29.8 26.4C29.8 25.4059 30.6059 24.6 31.6 24.6Z" fill="#1E3C38" />
</svg>
@if (cartItemCount() > 0) { @if (cartItemCount() > 0) {
<span class="cart-badge">{{ cartItemCount() }}</span> <span class="dexar-cart-badge">{{ cartItemCount() }}</span>
} }
</a> </a>
<div class="header-actions"> <!-- Language Selector (desktop only) -->
<div class="dexar-lang-selector dexar-lang-desktop">
<app-language-selector /> <app-language-selector />
</div> </div>
<button class="menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen"> <!-- Mobile Menu Toggle -->
<button class="dexar-menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen">
<span></span> <span></span>
<span></span> <span></span>
<span></span> <span></span>
</button> </button>
</div> </div>
</div>
</header> </header>
<!-- Mobile Menu Backdrop (outside header to blur page content) -->
@if (menuOpen) {
<div class="dexar-menu-backdrop" (click)="closeMenu()"></div>
}
<!-- Mobile Menu Panel (outside header for correct stacking) -->
<div class="dexar-mobile-menu" [class.dexar-mobile-menu-open]="menuOpen">
<a [routerLink]="'/' | langRoute" routerLinkActive="dexar-mobile-active" [routerLinkActiveOptions]="{exact: true}"
(click)="closeMenu()" class="dexar-mobile-item">
<svg width="24" height="24" viewBox="0 0 31 31" fill="none">
<path d="M16.185 2.22124C15.8067 1.84292 15.1933 1.84292 14.815 2.22124L3.18999 13.8462C3.00831 14.0279 2.90625 14.2743 2.90625 14.5312V28.0938C2.90625 28.6288 3.33997 29.0625 3.875 29.0625H12.5938C13.1288 29.0625 13.5625 28.6288 13.5625 28.0938V20.3438H17.4375V28.0938C17.4375 28.6288 17.8712 29.0625 18.4062 29.0625H27.125C27.66 29.0625 28.0938 28.6288 28.0938 28.0938V14.5312C28.0938 14.2743 27.9917 14.0279 27.81 13.8462L25.1875 11.2237V4.84375C25.1875 4.30872 24.7538 3.875 24.2188 3.875H22.2812C21.7462 3.875 21.3125 4.30872 21.3125 4.84375V7.34873L16.185 2.22124ZM4.84375 27.125V14.9325L15.5 4.27627L26.1562 14.9325V27.125H19.375V19.375C19.375 18.84 18.9413 18.4062 18.4062 18.4062H12.5938C12.0587 18.4062 11.625 18.84 11.625 19.375V27.125H4.84375Z" fill="#497671" />
</svg>
<span>{{ 'header.home' | translate }}</span>
<svg class="dexar-mobile-chevron" width="8" height="14" viewBox="0 0 8 14" fill="none">
<path d="M1 1L7 7L1 13" stroke="#697777" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
<a (click)="navigateToCatalog()" class="dexar-mobile-item" style="cursor: pointer;">
<svg width="24" height="24" viewBox="0 0 31 31" fill="none">
<path d="M1.9375 4.84375C1.9375 3.23867 3.23867 1.9375 4.84375 1.9375L10.6562 1.9375C12.2613 1.9375 13.5625 3.23867 13.5625 4.84375V10.6562C13.5625 12.2613 12.2613 13.5625 10.6562 13.5625H4.84375C3.23867 13.5625 1.9375 12.2613 1.9375 10.6562L1.9375 4.84375ZM4.84375 3.875C4.30872 3.875 3.875 4.30872 3.875 4.84375V10.6562C3.875 11.1913 4.30872 11.625 4.84375 11.625H10.6562C11.1913 11.625 11.625 11.1913 11.625 10.6562V4.84375C11.625 4.30872 11.1913 3.875 10.6562 3.875H4.84375ZM17.4375 4.84375C17.4375 3.23867 18.7387 1.9375 20.3438 1.9375L26.1562 1.9375C27.7613 1.9375 29.0625 3.23867 29.0625 4.84375V10.6562C29.0625 12.2613 27.7613 13.5625 26.1562 13.5625H20.3438C18.7387 13.5625 17.4375 12.2613 17.4375 10.6562V4.84375ZM20.3438 3.875C19.8087 3.875 19.375 4.30872 19.375 4.84375V10.6562C19.375 11.1913 19.8087 11.625 20.3438 11.625H26.1562C26.6913 11.625 27.125 11.1913 27.125 10.6562V4.84375C27.125 4.30872 26.6913 3.875 26.1562 3.875H20.3438ZM1.9375 20.3438C1.9375 18.7387 3.23867 17.4375 4.84375 17.4375H10.6562C12.2613 17.4375 13.5625 18.7387 13.5625 20.3438V26.1562C13.5625 27.7613 12.2613 29.0625 10.6562 29.0625H4.84375C3.23867 29.0625 1.9375 27.7613 1.9375 26.1562L1.9375 20.3438ZM4.84375 19.375C4.30872 19.375 3.875 19.8087 3.875 20.3438V26.1562C3.875 26.6913 4.30872 27.125 4.84375 27.125H10.6562C11.1913 27.125 11.625 26.6913 11.625 26.1562V20.3438C11.625 19.8087 11.1913 19.375 10.6562 19.375H4.84375ZM17.4375 20.3438C17.4375 18.7387 18.7387 17.4375 20.3438 17.4375H26.1562C27.7613 17.4375 29.0625 18.7387 29.0625 20.3438V26.1562C29.0625 27.7613 27.7613 29.0625 26.1562 29.0625H20.3438C18.7387 29.0625 17.4375 27.7613 17.4375 26.1562V20.3438ZM20.3438 19.375C19.8087 19.375 19.375 19.8087 19.375 20.3438V26.1562C19.375 26.6913 19.8087 27.125 20.3438 27.125H26.1562C26.6913 27.125 27.125 26.6913 27.125 26.1562V20.3438C27.125 19.8087 26.6913 19.375 26.1562 19.375H20.3438Z" fill="#497671" />
</svg>
<span>{{ 'header.catalog' | translate }}</span>
<svg class="dexar-mobile-chevron" width="8" height="14" viewBox="0 0 8 14" fill="none">
<path d="M1 1L7 7L1 13" stroke="#697777" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
<a [routerLink]="'/about' | langRoute" routerLinkActive="dexar-mobile-active" (click)="closeMenu()" class="dexar-mobile-item">
<svg width="24" height="24" viewBox="0 0 31 31" fill="none">
<path d="M27.125 1.9375C28.1951 1.9375 29.0625 2.80495 29.0625 3.875V27.125C29.0625 28.1951 28.1951 29.0625 27.125 29.0625H3.875C2.80495 29.0625 1.9375 28.1951 1.9375 27.125V3.875C1.9375 2.80495 2.80495 1.9375 3.875 1.9375H27.125ZM3.875 0C1.7349 0 0 1.7349 0 3.875V27.125C0 29.2651 1.7349 31 3.875 31H27.125C29.2651 31 31 29.2651 31 27.125V3.875C31 1.7349 29.2651 0 27.125 0H3.875Z" fill="#497671" />
<path d="M17.3032 12.764L12.8644 13.3203L12.7055 14.0582L13.5796 14.2172C14.1472 14.3534 14.2608 14.5577 14.1359 15.1254L12.7055 21.8461C12.3308 23.583 12.9098 24.4004 14.2721 24.4004C15.3279 24.4004 16.554 23.9122 17.1102 23.2424L17.2805 22.4364C16.8945 22.777 16.3269 22.9132 15.9523 22.9132C15.4187 22.9132 15.2257 22.5386 15.362 21.8801L17.3032 12.764Z" fill="#497671" />
<path d="M17.4375 8.71875C17.4375 9.7888 16.5701 10.6562 15.5 10.6562C14.4299 10.6562 13.5625 9.7888 13.5625 8.71875C13.5625 7.6487 14.4299 6.78125 15.5 6.78125C16.5701 6.78125 17.4375 7.6487 17.4375 8.71875Z" fill="#497671" />
</svg>
<span>{{ 'header.about' | translate }}</span>
<svg class="dexar-mobile-chevron" width="8" height="14" viewBox="0 0 8 14" fill="none">
<path d="M1 1L7 7L1 13" stroke="#697777" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
<a [routerLink]="'/contacts' | langRoute" routerLinkActive="dexar-mobile-active" (click)="closeMenu()" class="dexar-mobile-item">
<svg width="24" height="24" viewBox="0 0 31 31" fill="none">
<path d="M0 7.75C0 5.6099 1.7349 3.875 3.875 3.875H27.125C29.2651 3.875 31 5.6099 31 7.75V23.25C31 25.3901 29.2651 27.125 27.125 27.125H3.875C1.7349 27.125 0 25.3901 0 23.25V7.75ZM3.875 5.8125C2.80495 5.8125 1.9375 6.67995 1.9375 7.75V8.17025L15.5 16.3078L29.0625 8.17025V7.75C29.0625 6.67995 28.1951 5.8125 27.125 5.8125H3.875ZM29.0625 10.4297L19.9406 15.9029L29.0625 21.5164V10.4297ZM28.9971 23.7511L18.0688 17.026L15.5 18.5672L12.9312 17.026L2.00292 23.7511C2.22375 24.5782 2.97823 25.1875 3.875 25.1875H27.125C28.0218 25.1875 28.7762 24.5782 28.9971 23.7511ZM1.9375 21.5164L11.0594 15.9029L1.9375 10.4297V21.5164Z" fill="#497671" />
</svg>
<span>{{ 'header.contacts' | translate }}</span>
<svg class="dexar-mobile-chevron" width="8" height="14" viewBox="0 0 8 14" fill="none">
<path d="M1 1L7 7L1 13" stroke="#697777" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
<!-- Language Selector in mobile menu -->
<div class="dexar-mobile-lang">
<app-language-selector />
</div>
</div>
} }

View File

@@ -78,12 +78,12 @@
&:hover { &:hover {
color: var(--primary-hover); color: var(--primary-hover);
background: rgba(85, 104, 211, 0.05); background: rgba(73, 118, 113, 0.05);
} }
&.active { &.active {
color: var(--primary-hover); color: var(--primary-hover);
background: rgba(85, 104, 211, 0.1); background: rgba(73, 118, 113, 0.1);
font-weight: 600; font-weight: 600;
} }
} }
@@ -183,11 +183,11 @@
} }
&:hover { &:hover {
background: rgba(85, 104, 211, 0.05); background: rgba(73, 118, 113, 0.05);
} }
&.active { &.active {
background: rgba(85, 104, 211, 0.1); background: rgba(73, 118, 113, 0.1);
} }
} }
@@ -449,3 +449,488 @@
font-size: 1.1rem; font-size: 1.1rem;
} }
} }
// ========== DEXAR REDESIGN 2026 STYLES ==========
.dexar-header {
background: rgba(117, 121, 124, 0.1);
padding: 8px 0;
position: sticky;
top: 0;
z-index: 1000;
backdrop-filter: blur(10px);
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.dexar-header-container {
max-width: 1440px;
margin: 0 auto;
padding: 0 40px;
display: flex;
align-items: center;
gap: 32px;
height: 48px;
}
.dexar-logo {
display: flex;
align-items: center;
text-decoration: none;
flex-shrink: 0;
::ng-deep .logo-img {
width: 120px;
height: 38px;
object-fit: contain;
}
}
// Navigation Buttons Group
.dexar-nav {
display: flex;
flex-shrink: 0;
}
.dexar-nav-group {
display: flex;
align-items: center;
}
.dexar-nav-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 32px;
height: 38px;
border: 1px solid #d3dad9;
background: rgba(255, 255, 255, 0.74);
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600;
font-size: 15px;
color: #1e3c38;
text-decoration: none;
cursor: pointer;
transition: background 0.3s ease;
white-space: nowrap;
&:hover {
background: #a1b4b5;
color: #1e3c38;
}
&.dexar-active {
background: #497671;
color: #ffffff;
}
}
.dexar-nav-btn-left {
border-radius: 10px 0 0 10px;
padding: 6px 32px;
}
.dexar-nav-btn-middle {
padding: 6px 40px;
border-left: none;
}
.dexar-nav-btn-right {
border-radius: 0 10px 10px 0;
padding: 6px 28px;
border-left: none;
}
// Search Box
.dexar-search-wrapper {
flex: 1;
max-width: 200px;
margin-left: auto;
}
.dexar-search-box {
position: relative;
width: 100%;
height: 38px;
background: rgba(255, 255, 255, 0.74);
border: 1px solid #d2dad9;
border-radius: 19px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.12);
display: flex;
align-items: center;
padding: 0 14px;
gap: 8px;
}
.dexar-search-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.dexar-search-input {
flex: 1;
border: none;
background: transparent;
outline: none;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600;
font-size: 15px;
color: #828e8d;
cursor: pointer;
&::placeholder {
color: #828e8d;
}
}
// Actions
.dexar-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.dexar-cart-btn {
position: relative;
width: 36px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
cursor: pointer;
transition: opacity 0.3s ease;
svg {
width: 36px;
height: 28px;
}
&:hover {
opacity: 0.85;
}
&.dexar-cart-active {
opacity: 1;
}
}
.dexar-cart-badge {
position: absolute;
top: -8px;
right: -10px;
background: #497671;
color: #ffffff;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 700;
font-size: 10px;
line-height: 1;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.dexar-lang-selector {
display: flex;
align-items: center;
}
// Mobile Menu Backdrop
.dexar-menu-backdrop {
display: none;
}
// Mobile Menu Panel
.dexar-mobile-menu {
display: none;
}
// Mobile Menu Items
.dexar-mobile-item {
display: flex;
align-items: center;
gap: 14px;
width: 395px;
max-width: 100%;
height: 42px;
padding: 15px 26px;
background: #e9edf1;
border: 1px solid #d3dad9;
border-radius: 13px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
font-family: 'DM Sans', sans-serif;
font-size: 16px;
font-weight: 500;
color: #1e3c38;
text-decoration: none;
cursor: pointer;
transition: background 0.2s ease, transform 0.15s ease;
box-sizing: border-box;
&:hover {
background: #dce2e7;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
&.dexar-mobile-active {
background: #497671;
color: #ffffff;
border-color: #497671;
svg path {
fill: #ffffff;
}
.dexar-mobile-chevron path {
stroke: #ffffff;
}
}
svg:first-child {
flex-shrink: 0;
width: 24px;
height: 24px;
}
span {
flex: 1;
}
.dexar-mobile-chevron {
flex-shrink: 0;
margin-left: auto;
}
}
.dexar-mobile-lang {
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
}
.dexar-menu-toggle {
display: none;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 8px;
span {
width: 25px;
height: 3px;
background: #1e3c38;
border-radius: 2px;
transition: all 0.3s;
}
&.active {
span:nth-child(1) {
transform: rotate(45deg) translate(7px, 4.5px);
}
span:nth-child(2) {
opacity: 0;
}
span:nth-child(3) {
transform: rotate(-45deg) translate(7px, -4.5px);
}
}
}
// Mobile Search Icon
.dexar-search-mobile {
display: none;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
padding: 6px;
margin-left: auto;
transition: opacity 0.2s ease;
&:hover {
opacity: 0.7;
}
}
// Responsive Design
@media (max-width: 1200px) {
.dexar-header-container {
padding: 0 32px;
gap: 32px;
}
.dexar-nav-btn {
font-size: 18px;
padding: 8px 32px !important;
}
.dexar-nav-btn-left {
padding: 8px 32px !important;
}
.dexar-nav-btn-middle {
padding: 8px 40px !important;
}
.dexar-nav-btn-right {
padding: 8px 28px !important;
}
}
@media (max-width: 992px) {
.dexar-header-container {
padding: 0 20px;
gap: 20px;
}
.dexar-logo ::ng-deep .logo-img {
width: 120px;
height: 40px;
}
.dexar-nav-btn {
font-size: 16px;
padding: 8px 24px !important;
}
.dexar-nav-btn-left {
padding: 8px 24px !important;
}
.dexar-nav-btn-middle {
padding: 8px 28px !important;
}
.dexar-nav-btn-right {
padding: 8px 20px !important;
}
.dexar-search-box {
height: 42px;
}
.dexar-search-input {
font-size: 18px;
}
}
@media (max-width: 768px) {
.dexar-header-container {
gap: 12px;
padding: 0 16px;
}
// Hide desktop nav
.dexar-nav {
display: none;
}
// Hide desktop language selector
.dexar-lang-desktop {
display: none;
}
.dexar-search-wrapper {
display: none;
}
.dexar-search-mobile {
display: flex;
}
.dexar-menu-toggle {
display: flex;
}
.dexar-logo ::ng-deep .logo-img {
width: 100px;
height: 32px;
}
// Mobile Menu Backdrop — full-screen, blurred, blocks interaction
.dexar-menu-backdrop {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.35);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
z-index: 998;
animation: dexar-fade-in 0.25s ease;
}
// Mobile Menu Panel
.dexar-mobile-menu {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
position: fixed;
top: 84px;
right: 0;
width: 485px;
max-width: 100vw;
padding: 28px 20px 32px;
background: rgba(245, 242, 249, 0.93);
border-radius: 0 0 13px 13px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.25);
z-index: 999;
box-sizing: border-box;
// Hide by default, animate in
max-height: 0;
padding-top: 0;
padding-bottom: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.35s ease, opacity 0.25s ease, padding 0.35s ease;
&.dexar-mobile-menu-open {
max-height: 576px;
padding: 28px 20px 32px;
opacity: 1;
}
}
.dexar-mobile-item {
width: 395px;
max-width: 100%;
}
.dexar-mobile-lang {
margin-top: 16px;
}
}
@media (max-width: 480px) {
.dexar-mobile-menu {
width: 100vw;
border-radius: 0 0 13px 13px;
}
.dexar-mobile-item {
width: 100%;
}
}
@keyframes dexar-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}

View File

@@ -1,17 +1,18 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy, Renderer2, inject, DOCUMENT } from '@angular/core';
import { CommonModule } from '@angular/common'; import { Router, RouterLink, RouterLinkActive } from '@angular/router';
import { RouterLink, RouterLinkActive } from '@angular/router'; import { CartService, LanguageService } from '../../services';
import { CartService } from '../../services';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { LogoComponent } from '../logo/logo.component'; import { LogoComponent } from '../logo/logo.component';
import { LanguageSelectorComponent } from '../language-selector/language-selector.component'; import { LanguageSelectorComponent } from '../language-selector/language-selector.component';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
standalone: true, imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, LangRoutePipe, TranslatePipe],
imports: [CommonModule, RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent],
templateUrl: './header.component.html', templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'] styleUrls: ['./header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class HeaderComponent { export class HeaderComponent {
cartItemCount; cartItemCount;
@@ -20,15 +21,40 @@ export class HeaderComponent {
logo = environment.logo; logo = environment.logo;
isnovo = environment.theme === 'novo'; isnovo = environment.theme === 'novo';
constructor(private cartService: CartService) { private renderer = inject(Renderer2);
private document = inject(DOCUMENT);
private langService = inject(LanguageService);
constructor(private cartService: CartService, private router: Router) {
this.cartItemCount = this.cartService.itemCount; this.cartItemCount = this.cartService.itemCount;
} }
toggleMenu(): void { toggleMenu(): void {
this.menuOpen = !this.menuOpen; this.menuOpen = !this.menuOpen;
if (this.menuOpen) {
this.renderer.addClass(this.document.body, 'dexar-menu-open');
} else {
this.renderer.removeClass(this.document.body, 'dexar-menu-open');
}
} }
closeMenu(): void { closeMenu(): void {
this.menuOpen = false; this.menuOpen = false;
this.renderer.removeClass(this.document.body, 'dexar-menu-open');
}
navigateToSearch(): void {
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}/search`]);
}
navigateToCatalog(): void {
this.closeMenu();
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]).then(() => {
setTimeout(() => {
this.document.getElementById('catalog')?.scrollIntoView({ behavior: 'smooth' });
}, 100);
});
} }
} }

View File

@@ -2,7 +2,7 @@
@if (loading()) { @if (loading()) {
<div class="carousel-loading"> <div class="carousel-loading">
<div class="spinner"></div> <div class="spinner"></div>
<p>Загрузка товаров...</p> <p>{{ 'carousel.loading' | translate }}</p>
</div> </div>
} @else if (products().length > 0) { } @else if (products().length > 0) {
<p-carousel <p-carousel
@@ -16,21 +16,26 @@
[showIndicators]="true"> [showIndicators]="true">
<ng-template let-product pTemplate="item"> <ng-template let-product pTemplate="item">
<div class="item-card"> <div class="item-card">
<a [routerLink]="['/item', product.itemID]" class="item-link"> <a [routerLink]="['/item', product.itemID] | langRoute" class="item-link">
<div class="item-image"> <div class="item-image">
<img [src]="getItemImage(product)" [alt]="product.name" loading="lazy" /> <img [src]="getItemImage(product)" [alt]="product.name" loading="lazy" />
@if (product.discount > 0) { @if (product.discount > 0) {
<div class="discount-badge">-{{ product.discount }}%</div> <span class="discount-badge">-{{ product.discount }}%</span>
} }
</div> </div>
<div class="item-details"> <div class="item-details">
<h3 class="item-name">{{ product.name }}</h3> <h3 class="item-name">{{ product.name }}</h3>
<div class="item-rating" *ngIf="product.rating"> @if (product.rating) {
<span class="rating-stars">⭐ {{ product.rating }}</span> <div class="item-rating">
<svg width="14" height="14" viewBox="0 0 24 24" fill="#497671" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/>
</svg>
<span class="rating-value">{{ product.rating }}</span>
<span class="rating-count">({{ product.callbacks?.length || 0 }})</span> <span class="rating-count">({{ product.callbacks?.length || 0 }})</span>
</div> </div>
}
<div class="item-price-row"> <div class="item-price-row">
<div class="item-price"> <div class="item-price">
@@ -41,20 +46,18 @@
<span class="current-price">{{ product.price }} {{ product.currency }}</span> <span class="current-price">{{ product.price }} {{ product.currency }}</span>
} }
</div> </div>
<button class="cart-icon-btn" (click)="addToCart($event, product)" title="Добавить в корзину"> <button class="cart-icon-btn" (click)="addToCart($event, product)" [title]="'carousel.addToCart' | translate">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="9" cy="21" r="1"></circle> <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path>
<circle cx="20" cy="21" r="1"></circle> <line x1="3" y1="6" x2="21" y2="6"></line>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path> <path d="M16 10a4 4 0 0 1-8 0"></path>
</svg> </svg>
</button> </button>
</div> </div>
</div> </div>
</a> </a>
</div> </div>
</ng-template> </ng-template>
<ul class="p-carousel-indicator-list" data-pc-section="indicatorlist"><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="1" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="2" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="3" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="4" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="5" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="6" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator p-carousel-indicator-active" data-p-active="true" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="0" aria-label="7" aria-current="page" data-pc-section="indicatorbutton"></button></li><!----></ul>
</p-carousel> </p-carousel>
} }
</div> </div>

View File

@@ -3,6 +3,7 @@
padding: 2rem 0; padding: 2rem 0;
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
::ng-deep { ::ng-deep {
// PrimeNG carousel wrapper // PrimeNG carousel wrapper
@@ -21,8 +22,8 @@
height: 3rem; height: 3rem;
border-radius: 50%; border-radius: 50%;
background: white; background: white;
border: 2px solid #e5e7eb; border: 2px solid #d3dad9;
color: #374151; color: #1e3c38;
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
align-self: center; align-self: center;
@@ -31,13 +32,13 @@
&:hover { &:hover {
background: #f9fafb; background: #f9fafb;
border-color: #d1d5db; border-color: #d3dad9;
transform: scale(1.05); transform: scale(1.05);
} }
&:not(:disabled):hover { &:not(:disabled):hover {
background: var(--primary-color, #5568d3); background: var(--primary-color, #497671);
border-color: var(--primary-color, #5568d3); border-color: var(--primary-color, #497671);
color: white; color: white;
} }
} }
@@ -63,7 +64,7 @@
flex-shrink: 0; flex-shrink: 0;
} }
// Pagination dots - using actual PrimeNG rendered classes // Pagination dots
.p-carousel-indicator-list { .p-carousel-indicator-list {
display: flex !important; display: flex !important;
justify-content: center !important; justify-content: center !important;
@@ -79,20 +80,20 @@
width: 12px !important; width: 12px !important;
height: 12px !important; height: 12px !important;
border-radius: 50% !important; border-radius: 50% !important;
background-color: #d1d5db !important; background-color: #d3dad9 !important;
border: 0 !important; border: 0 !important;
padding: 0 !important; padding: 0 !important;
cursor: pointer !important; cursor: pointer !important;
transition: all 0.3s ease !important; transition: all 0.3s ease !important;
&:hover { &:hover {
background-color: #9ca3af !important; background-color: #a1b4b5 !important;
transform: scale(1.2); transform: scale(1.2);
} }
} }
&.p-carousel-indicator-active .p-carousel-indicator-button { &.p-carousel-indicator-active .p-carousel-indicator-button {
background-color: var(--primary-color, #5568d3) !important; background-color: var(--primary-color, #497671) !important;
width: 32px !important; width: 32px !important;
border-radius: 6px !important; border-radius: 6px !important;
} }
@@ -106,14 +107,14 @@
.carousel-empty { .carousel-empty {
text-align: center; text-align: center;
padding: 3rem 1rem; padding: 3rem 1rem;
color: #666; color: #697777;
.spinner { .spinner {
width: 40px; width: 40px;
height: 40px; height: 40px;
margin: 0 auto 1rem; margin: 0 auto 1rem;
border: 4px solid #f3f3f3; border: 4px solid #f3f3f3;
border-top: 4px solid var(--primary-color, #5568d3); border-top: 4px solid var(--primary-color, #497671);
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
@@ -124,13 +125,14 @@
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
// Item card styles matching your existing design // Item card styles
.item-card { .item-card {
background: white; background: #ffffff;
border-radius: 12px; border-radius: 13px;
border: 1px solid #d3dad9;
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease; transition: box-shadow 0.3s ease, transform 0.3s ease;
position: relative; position: relative;
height: 100%; height: 100%;
display: flex; display: flex;
@@ -140,7 +142,7 @@
margin: 0 auto; margin: 0 auto;
&:hover { &:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 28px rgba(0, 0, 0, 0.12);
transform: translateY(-4px); transform: translateY(-4px);
} }
} }
@@ -156,78 +158,86 @@
.item-image { .item-image {
position: relative; position: relative;
width: 100%; width: 100%;
height: 140px; height: 170px;
overflow: hidden; overflow: hidden;
background: #f5f5f5; background: #f5f3f9;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
background: white; padding: 12px;
transition: transform 0.3s ease; transition: transform 0.4s ease;
box-sizing: border-box;
} }
&:hover img { .item-card:hover & img {
transform: scale(1.08); transform: scale(1.06);
} }
} }
.discount-badge { .discount-badge {
position: absolute; position: absolute;
top: 12px; top: 10px;
right: 12px; left: 10px;
background: #e74c3c; background: #ef4444;
color: white; color: white;
padding: 0.4rem 0.8rem; padding: 4px 10px;
border-radius: 6px; border-radius: 20px;
font-size: 0.875rem; font-family: "DM Sans", sans-serif;
font-size: 0.75rem;
font-weight: 700; font-weight: 700;
z-index: 10; letter-spacing: 0.02em;
z-index: 2;
line-height: 1.3;
} }
.item-details { .item-details {
padding: 0.625rem; padding: 12px 14px 14px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.375rem; gap: 6px;
flex: 1; flex: 1;
} }
.item-name { .item-name {
font-family: "DM Sans", sans-serif;
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 600; font-weight: 600;
margin: 0; margin: 0;
line-height: 1.3; line-height: 1.35;
min-height: 2.6em; min-height: 2.2em;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2; line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
color: #1a1a1a; color: #1e3c38;
transition: color 0.2s; transition: color 0.2s;
&:hover { .item-card:hover & {
color: var(--primary-color, #5568d3); color: #497671;
} }
} }
.item-rating { .item-rating {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.25rem; gap: 4px;
font-family: "DM Sans", sans-serif;
font-size: 0.75rem; font-size: 0.75rem;
.rating-stars { svg {
color: #fbbf24; flex-shrink: 0;
}
.rating-value {
font-weight: 600; font-weight: 600;
color: #1e3c38;
} }
.rating-count { .rating-count {
color: #6b7280; color: #697777;
} }
} }
@@ -235,42 +245,47 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; gap: 8px;
margin-top: auto;
padding-top: 4px;
} }
.item-price { .item-price {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 0.375rem; gap: 6px;
flex-wrap: wrap; flex-wrap: wrap;
flex: 1; flex: 1;
font-family: "DM Sans", sans-serif;
.current-price, .current-price,
.discounted-price { .discounted-price {
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 700; font-weight: 700;
color: #1a1a1a; color: #1e3c38;
} }
.discounted-price { .discounted-price {
color: #e74c3c; color: #ef4444;
} }
.original-price { .original-price {
font-size: 0.8125rem; font-size: 0.75rem;
color: #9ca3af; color: #a1b4b5;
text-decoration: line-through; text-decoration: line-through;
} }
} }
.cart-icon-btn { .cart-icon-btn {
padding: 0.5rem; width: 36px;
background: var(--primary-color, #5568d3); height: 36px;
padding: 0;
background: #497671;
color: white; color: white;
border: none; border: none;
border-radius: 8px; border-radius: 10px;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: background 0.2s ease, transform 0.15s ease;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -278,11 +293,13 @@
svg { svg {
display: block; display: block;
width: 18px;
height: 18px;
} }
&:hover { &:hover {
background: var(--primary-hover, #4456b3); background: #3d635f;
transform: scale(1.05); transform: scale(1.08);
} }
&:active { &:active {
@@ -297,14 +314,14 @@
.p-carousel-prev, .p-carousel-prev,
.p-carousel-next { .p-carousel-next {
&:not(:disabled):hover { &:not(:disabled):hover {
background: var(--primary-color, #5568d3); background: var(--primary-color, #497671);
border-color: var(--primary-color, #5568d3); border-color: var(--primary-color, #497671);
} }
} }
.p-carousel-indicators { .p-carousel-indicators {
.p-carousel-indicator.p-highlight button { .p-carousel-indicator.p-highlight button {
background: var(--primary-color, #5568d3); background: var(--primary-color, #497671);
} }
} }
} }
@@ -315,10 +332,10 @@
} }
.cart-icon-btn { .cart-icon-btn {
background: var(--primary-color, #5568d3); background: var(--primary-color, #497671);
&:hover { &:hover {
background: var(--primary-hover, #4456b3); background: var(--primary-hover, #3d635f);
} }
} }
} }
@@ -330,27 +347,27 @@
} }
.item-image { .item-image {
height: 220px; height: 160px;
} }
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.item-image { .item-image {
height: 200px; height: 150px;
} }
.item-details { .item-details {
padding: 1rem; padding: 10px 12px 12px;
} }
.item-name { .item-name {
font-size: 0.9rem; font-size: 0.8125rem;
} }
.item-price { .item-price {
.current-price, .current-price,
.discounted-price { .discounted-price {
font-size: 1.125rem; font-size: 1rem;
} }
} }
} }

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, signal } from '@angular/core'; import { Component, OnInit, signal, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { DecimalPipe } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { CarouselModule } from 'primeng/carousel'; import { CarouselModule } from 'primeng/carousel';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
@@ -7,20 +7,23 @@ import { TagModule } from 'primeng/tag';
import { ApiService, CartService } from '../../services'; import { ApiService, CartService } from '../../services';
import { Item } from '../../models'; import { Item } from '../../models';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { getDiscountedPrice, getMainImage } from '../../utils/item.utils';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
@Component({ @Component({
selector: 'app-items-carousel', selector: 'app-items-carousel',
templateUrl: './items-carousel.component.html', templateUrl: './items-carousel.component.html',
standalone: true, imports: [DecimalPipe, RouterLink, CarouselModule, ButtonModule, TagModule, LangRoutePipe, TranslatePipe],
imports: [CommonModule, RouterLink, CarouselModule, ButtonModule, TagModule], styleUrls: ['./items-carousel.component.scss'],
styleUrls: ['./items-carousel.component.scss'] changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ItemsCarouselComponent implements OnInit { export class ItemsCarouselComponent implements OnInit {
products = signal<Item[]>([]); products = signal<Item[]>([]);
loading = signal(true); loading = signal(true);
isnovo = environment.theme === 'novo'; isnovo = environment.theme === 'novo';
responsiveOptions: any[] | undefined; responsiveOptions: { breakpoint: string; numVisible: number; numScroll: number }[] | undefined;
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
@@ -67,7 +70,7 @@ export class ItemsCarouselComponent implements OnInit {
]; ];
} }
getSeverity(remainings: string) { getSeverity(remainings: string): 'success' | 'info' | 'warn' | 'danger' | 'secondary' | 'contrast' {
switch (remainings) { switch (remainings) {
case 'high': case 'high':
return 'success'; return 'success';
@@ -93,19 +96,8 @@ export class ItemsCarouselComponent implements OnInit {
} }
} }
getItemImage(item: Item): string { readonly getItemImage = getMainImage;
if (item.photos && item.photos.length > 0 && item.photos[0]?.url) { readonly getDiscountedPrice = getDiscountedPrice;
return item.photos[0].url;
}
return '/assets/images/placeholder.jpg';
}
getDiscountedPrice(item: Item): number {
if (item.discount > 0) {
return item.price * (1 - item.discount / 100);
}
return item.price;
}
addToCart(event: Event, item: Item): void { addToCart(event: Event, item: Item): void {
event.preventDefault(); event.preventDefault();

View File

@@ -152,6 +152,74 @@
} }
} }
// Dexar header specific styles
:host-context(.dexar-header),
:host-context(.dexar-mobile-menu) {
.language-selector {
width: 56px;
height: 28px;
}
.language-button {
width: 100%;
height: 100%;
padding: 4px;
gap: 4px;
background: rgba(255, 255, 255, 0.3);
border: 1px solid #677b78;
border-radius: 8px;
color: #1e3c38;
justify-content: center;
&:hover {
background: rgba(255, 255, 255, 0.5);
border-color: #677b78;
}
.language-flag {
display: none;
}
.language-code {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 500;
font-size: 15px;
color: #1e3c38;
letter-spacing: 0;
line-height: 1;
}
.dropdown-arrow {
display: block;
width: 7px;
height: 10px;
opacity: 1;
path {
stroke: #1e3c38;
}
}
}
.language-dropdown {
background: #ffffff;
border-color: #d3dad9;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.language-option {
color: #1e3c38;
&:hover:not(.disabled) {
background: rgba(161, 180, 181, 0.2);
}
&.active {
background: rgba(73, 118, 113, 0.1);
}
}
}
// Mobile responsiveness // Mobile responsiveness
@media (max-width: 768px) { @media (max-width: 768px) {
.language-button { .language-button {
@@ -177,4 +245,16 @@
height: 20px; height: 20px;
} }
} }
:host-context(.dexar-header),
:host-context(.dexar-mobile-menu) {
.language-selector {
width: 56px;
height: 28px;
}
.language-button .language-code {
font-size: 18px;
}
}
} }

View File

@@ -1,13 +1,12 @@
import { Component, HostListener, ElementRef } from '@angular/core'; import { Component, HostListener, ElementRef, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LanguageService, Language } from '../../services/language.service'; import { LanguageService, Language } from '../../services/language.service';
@Component({ @Component({
selector: 'app-language-selector', selector: 'app-language-selector',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './language-selector.component.html', templateUrl: './language-selector.component.html',
styleUrls: ['./language-selector.component.scss'] styleUrls: ['./language-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class LanguageSelectorComponent { export class LanguageSelectorComponent {
dropdownOpen = false; dropdownOpen = false;
@@ -23,7 +22,7 @@ export class LanguageSelectorComponent {
selectLanguage(lang: Language): void { selectLanguage(lang: Language): void {
if (lang.enabled) { if (lang.enabled) {
this.languageService.setLanguage(lang.code); this.languageService.switchLanguage(lang.code);
this.dropdownOpen = false; this.dropdownOpen = false;
} }
} }

View File

@@ -1,16 +1,17 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
@Component({ @Component({
selector: 'app-logo', selector: 'app-logo',
standalone: true, template: `<img [src]="logoPath" [alt]="brandName + ' logo'" class="logo-img" fetchpriority="high" />`,
template: `<img [src]="logoPath" [alt]="brandName + ' logo'" class="logo-img" width="120" height="40" fetchpriority="high" />`,
styles: [` styles: [`
.logo-img { .logo-img {
height: 40px; width: 100%;
width: auto; height: 100%;
object-fit: contain;
} }
`] `],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class LogoComponent { export class LogoComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -0,0 +1,28 @@
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { LanguageService } from '../services/language.service';
export const languageGuard: CanActivateFn = (route, state) => {
const langService = inject(LanguageService);
const router = inject(Router);
const lang = route.params['lang'];
const langObj = langService.languages.find(l => l.code === lang);
if (langObj?.enabled) {
// Valid and enabled language — set it and proceed
langService.setLanguage(lang);
return true;
}
const defaultLang = langService.currentLanguage();
if (langObj && !langObj.enabled) {
// Known but disabled language — redirect to default lang, keep the rest of the path
const pathAfterLang = state.url.slice(1 + lang.length);
return router.createUrlTree([`/${defaultLang}${pathAfterLang}`]);
}
// Not a recognized language code — treat as a legacy URL without lang prefix
return router.createUrlTree([`/${defaultLang}${state.url}`]);
};

188
src/app/i18n/en.ts Normal file
View File

@@ -0,0 +1,188 @@
import { Translations } from './translations';
export const en: Translations = {
header: {
home: 'Home',
search: 'Search',
about: 'About',
contacts: 'Contacts',
searchPlaceholder: 'Search...',
catalog: 'Catalog',
},
footer: {
description: 'A modern marketplace for comfortable shopping',
company: 'Company',
aboutUs: 'About us',
contacts: 'Contacts',
requisites: 'Company details',
support: 'Support',
faq: 'FAQ',
delivery: 'Delivery',
guarantee: 'Warranty',
legal: 'Legal information',
offer: 'Public offer',
privacy: 'Privacy policy',
returns: 'Returns',
info: 'Information',
aboutCompany: 'About company',
documents: 'Documents',
paymentRules: 'Payment terms',
returnPolicy: 'Return policy',
publicOffer: 'Public offer',
help: 'Help',
payment: 'Payment',
allRightsReserved: 'All rights reserved.',
},
home: {
welcomeTo: 'Welcome to {{brand}}',
subtitle: 'Find everything you need in one place',
startSearch: 'Start search',
loading: 'Loading categories...',
errorTitle: 'Something went wrong',
retry: 'Try again',
categoriesTitle: 'Product categories',
categoriesSubtitle: 'Choose a category',
categoriesEmpty: 'Categories coming soon',
categoriesEmptyDesc: 'We are working on filling the catalog',
dexarHeroTitle: 'Find everything here',
dexarHeroSubtitle: 'Thousands of products in one place',
dexarHeroTagline: 'simple and convenient',
goToCatalog: 'Go to catalog',
findProduct: 'Find a product',
loadingDexar: 'Loading categories...',
catalogTitle: 'Product catalog',
emptyCategoriesDexar: 'No categories yet',
categoriesSoonDexar: 'Categories will appear here soon',
itemsCount: '{{count}} products',
},
cart: {
title: 'Cart',
clear: 'Clear',
empty: 'Cart is empty',
emptyDesc: 'Add products to start shopping',
goShopping: 'Start shopping',
total: 'Total',
items: 'Products',
deliveryLabel: 'Delivery',
toPay: 'To pay',
agreeWith: 'I agree with the',
publicOffer: 'public offer',
returnPolicy: 'return policy',
guaranteeTerms: 'warranty terms',
privacyPolicy: 'privacy policy',
and: 'and',
checkout: 'Place order',
close: 'Close',
creatingPayment: 'Creating payment...',
waitFewSeconds: 'Please wait a few seconds',
scanQr: 'Scan the QR code to pay',
amountToPay: 'Amount due:',
waitingPayment: 'Waiting for payment...',
copied: '✓ Copied',
copyLink: 'Copy link',
openNewTab: 'Open in new tab',
paymentSuccess: 'Congratulations! Payment successful!',
paymentSuccessDesc: 'Enter your contact details and we will send your purchase within a few minutes',
sending: 'Sending...',
send: 'Send',
paymentTimeout: 'Payment timed out',
paymentTimeoutDesc: 'We did not receive payment confirmation within 3 minutes.',
autoClose: 'Window will close automatically...',
confirmClear: 'Are you sure you want to clear the cart?',
acceptTerms: 'Please accept the offer terms, return policy, and warranty terms to proceed with the order.',
copyError: 'Copy error:',
emailSuccess: 'Email sent successfully! Check your inbox.',
emailError: 'An error occurred while sending the email. Please try again.',
phoneRequired: 'Phone number is required',
phoneMoreDigits: 'Enter {{count}} more digits',
phoneTooMany: 'Too many digits',
emailRequired: 'Email is required',
emailTooShort: 'Email is too short (minimum 5 characters)',
emailTooLong: 'Email is too long (maximum 100 characters)',
emailNeedsAt: 'Email must contain @',
emailNeedsDomain: 'Email must contain a domain (.com, .ru, etc.)',
emailInvalid: 'Invalid email format',
},
search: {
title: 'Product search',
placeholder: 'Enter product name...',
resultsCount: 'Products found:',
searching: 'Searching...',
retry: 'Try again',
noResults: 'Nothing found',
noResultsFor: 'No products found for "{{query}}"',
noResultsHint: 'Try changing your query or using different keywords',
addToCart: 'Add to cart',
loadingMore: 'Loading...',
allLoaded: 'All results loaded',
emptyState: 'Enter a query to search for products',
of: 'of',
},
category: {
retry: 'Try again',
addToCart: 'Add to cart',
loadingMore: 'Loading...',
allLoaded: 'All products loaded',
emptyTitle: 'Oops! Nothing here yet',
emptyDesc: 'There are no products in this category yet, but they will appear soon',
goHome: 'Go home',
loading: 'Loading products...',
},
subcategories: {
loading: 'Loading subcategories...',
retry: 'Try again',
emptyTitle: 'Oops! No subcategories yet',
emptyDesc: 'There are no subcategories in this section yet, but they will appear soon',
goHome: 'Go home',
},
itemDetail: {
loading: 'Loading...',
loadingDexar: 'Loading product...',
back: 'Go back',
backHome: 'Back to home',
noImage: 'No image',
stock: 'Availability:',
inStock: 'In stock',
lowStock: 'Few left',
lastItems: 'Last items',
mediumStock: 'Running low',
addToCart: 'Add to cart',
description: 'Description',
reviews: 'Reviews',
yourReview: 'Your review',
leaveReview: 'Leave a review',
rating: 'Rating:',
reviewPlaceholder: 'Share your experience with this product...',
reviewPlaceholderDexar: 'Share your thoughts on this product...',
anonymous: 'Anonymous',
submitting: 'Submitting...',
submit: 'Submit',
reviewSuccess: 'Thank you for your review!',
reviewError: 'Submission failed. Please try later.',
defaultUser: 'User',
defaultUserDexar: 'Anonymous',
noReviews: 'No reviews yet. Be the first!',
qna: 'Questions & Answers',
photo: 'Photo',
reviewsCount: 'reviews',
today: 'Today',
yesterday: 'Yesterday',
daysAgo: 'd. ago',
weeksAgo: 'w. ago',
},
app: {
connecting: 'Connecting to server...',
serverUnavailable: 'Server unavailable',
serverError: 'Could not connect to the server. Please check your internet connection.',
retryConnection: 'Retry',
pageTitle: 'Marketplace of goods and services',
},
carousel: {
loading: 'Loading products...',
addToCart: 'Add to cart',
},
common: {
retry: 'Try again',
loading: 'Loading...',
},
};

188
src/app/i18n/hy.ts Normal file
View File

@@ -0,0 +1,188 @@
import { Translations } from './translations';
export const hy: Translations = {
header: {
home: 'Գլխավոր',
search: 'Որոնում',
about: 'Մեր մասին',
contacts: 'Կապ',
searchPlaceholder: 'Որոնել...',
catalog: 'Կատալոգ',
},
footer: {
description: 'Ժամանակակից մարքեթփլեյս հարմար գնումների համար',
company: 'Ընկերություն',
aboutUs: 'Մեր մասին',
contacts: 'Կապ',
requisites: 'Վավերապայմաններ',
support: 'Աջակցություն',
faq: 'ՀՏՀ',
delivery: 'Առաքում',
guarantee: 'Երաշխիք',
legal: 'Իրավական տեղեկատվություն',
offer: 'Օֆերտա',
privacy: 'Գաղտնիություն',
returns: 'Վերադարձ',
info: 'Տեղեկատվություն',
aboutCompany: 'Ընկերության մասին',
documents: 'Փաստաթղթեր',
paymentRules: 'Վճարման կանոններ',
returnPolicy: 'Վերադարձի քաղաքականություն',
publicOffer: 'Հանրային օֆերտա',
help: 'Օգնություն',
payment: 'Վճարում',
allRightsReserved: 'Բոլոր իրավունքները պաշտպանված են։',
},
home: {
welcomeTo: 'Բարի գալուստ {{brand}}',
subtitle: 'Գտեք ամեն ինչ մեկ վայրում',
startSearch: 'Սկսել որոնումը',
loading: 'Կատեգորիաները բեռնվում են...',
errorTitle: 'Ինչ-որ բան սխալ է գնացել',
retry: 'Փորձել կրկին',
categoriesTitle: 'Ապրանքների կատեգորիաներ',
categoriesSubtitle: 'Ընտրեք հետաքրքրող կատեգորիան',
categoriesEmpty: 'Կատեգորիաները շուտով կհայտնվեն',
categoriesEmptyDesc: 'Մենք աշխատում ենք կատալոգի համալրման վրա',
dexarHeroTitle: 'Այստեղ դու կգտնես ամեն ինչ',
dexarHeroSubtitle: 'Հազարավոր ապրանքներ մեկ վայրում',
dexarHeroTagline: 'պարզ և հարմար',
goToCatalog: 'Անցնել կատալոգ',
findProduct: 'Գտնել ապրանք',
loadingDexar: 'Կատեգորիաները բեռնվում են...',
catalogTitle: 'Ապրանքների կատալոգ',
emptyCategoriesDexar: 'Կատեգորիաները դեռ չկան',
categoriesSoonDexar: 'Շուտով այստեղ կհայտնվեն ապրանքների կատեգորիաներ',
itemsCount: '{{count}} ապրանք',
},
cart: {
title: 'Զամբյուղ',
clear: 'Մաքրել',
empty: 'Զամբյուղը դատարկ է',
emptyDesc: 'Ավելացրեք ապրանքներ գնումները սկսելու համար',
goShopping: 'Անցնել գնումների',
total: 'Ընդամենը',
items: 'Ապրանքներ',
deliveryLabel: 'Առաքում',
toPay: 'Վճարման ենթակա',
agreeWith: 'Ես համաձայն եմ',
publicOffer: 'հանրային օֆերտային',
returnPolicy: 'վերադարձի քաղաքականությանը',
guaranteeTerms: 'երաշխիքային պայմաններին',
privacyPolicy: 'գաղտնիության քաղաքականությանը',
and: 'և',
checkout: 'Ձևակերպել պատվեր',
close: 'Փակել',
creatingPayment: 'Վճարումը ստեղծվում է...',
waitFewSeconds: 'Սպասեք մի քանի վայրկյան',
scanQr: 'Սկանավորեք QR կոդը վճարման համար',
amountToPay: 'Վճարման գումարը՝',
waitingPayment: 'Սպասում ենք վճարմանը...',
copied: '✓ Պատճենված է',
copyLink: 'Պատճենել հղումը',
openNewTab: 'Բացել նոր ներդիրում',
paymentSuccess: 'Շնորհավորում ենք։ Վճարումը հաջողությամբ կատարվել է։',
paymentSuccessDesc: 'Մուտքագրեք ձեր կոնտակտային տվյալները, և մենք կուղարկենք գնումը մի քանի րոպեի ընթացքում',
sending: 'Ուղարկվում է...',
send: 'Ուղարկել',
paymentTimeout: 'Սպասման ժամանակը սպառվել է',
paymentTimeoutDesc: 'Մենք չենք ստացել վճարման հաստատում 3 րոպեի ընթացքում։',
autoClose: 'Պատուհանը կփակվի ավտոմատ...',
confirmClear: 'Համոզվա՞ծ եք, որ ցանկանում եք մաքրել զամբյուղը։',
acceptTerms: 'Խնդրում ենք ընդունել օֆերտայի, վերադարձի և երաշխիքի պայմանները պատվերը հաստատելու համար։',
copyError: 'Պատճենման սխալ՝',
emailSuccess: 'Email-ը հաջողությամբ ուղարկվել է։ Ստուգեք ձեր փոստը։',
emailError: 'Email ուղարկելու ժամանակ տեղի ունեցավ սխալ։ Խնդրում ենք փորձել կրկին։',
phoneRequired: 'Հեռախոսահամարը պարտադիր է',
phoneMoreDigits: 'Մուտքագրեք ևս {{count}} թիվ',
phoneTooMany: 'Չափազանց շատ թվեր',
emailRequired: 'Email-ը պարտադիր է',
emailTooShort: 'Email-ը չափազանց կարճ է (նվազագույնը 5 նիշ)',
emailTooLong: 'Email-ը չափազանց երկար է (առավելագույնը 100 նիշ)',
emailNeedsAt: 'Email-ը պետք է պարունակի @ նշանը',
emailNeedsDomain: 'Email-ը պետք է պարունակի դոմեն (.com, .ru և այլն)',
emailInvalid: 'Email-ի ձևաչափը սխալ է',
},
search: {
title: 'Ապրանքների որոնում',
placeholder: 'Մուտքագրեք ապրանքի անունը...',
resultsCount: 'Գտնված ապրանքներ՝',
searching: 'Որոնում...',
retry: 'Փորձել կրկին',
noResults: 'Ոչինչ չի գտնվել',
noResultsFor: '"{{query}}" հարցման համար ապրանքներ չեն գտնվել',
noResultsHint: 'Փորձեք փոխել հարցումը կամ օգտագործել այլ բանալի բառեր',
addToCart: 'Ավելացնել զամբյուղ',
loadingMore: 'Բեռնվում է...',
allLoaded: 'Բոլոր արդյունքները բեռնված են',
emptyState: 'Մուտքագրեք հարցում ապրանքներ որոնելու համար',
of: 'ից',
},
category: {
retry: 'Փորձել կրկին',
addToCart: 'Ավելացնել զամբյուղ',
loadingMore: 'Բեռնվում է...',
allLoaded: 'Բոլոր ապրանքները բեռնված են',
emptyTitle: 'Ուպս։ Այստեղ դեռ դատարկ է',
emptyDesc: 'Այս կատեգորիայում դեռ ապրանքներ չկան, բայց շուտով կհայտնվեն',
goHome: 'Գլխավոր էջ',
loading: 'Ապրանքները բեռնվում են...',
},
subcategories: {
loading: 'Ենթակատեգորիաները բեռնվում են...',
retry: 'Փորձել կրկին',
emptyTitle: 'Ուպս։ Ենթակատեգորիաներ դեռ չկան',
emptyDesc: 'Այս բաժնում դեռ ենթակատեգորիաներ չկան, բայց շուտով կհայտնվեն',
goHome: 'Գլխավոր էջ',
},
itemDetail: {
loading: 'Բեռնվում է...',
loadingDexar: 'Ապրանքը բեռնվում է...',
back: 'Վերադառնալ',
backHome: 'Վերադառնալ գլխավոր էջ',
noImage: 'Պատկեր չկա',
stock: 'Առկայություն՝',
inStock: 'Առկա է',
lowStock: 'Մնացել է քիչ',
lastItems: 'Վերջին հատերը',
mediumStock: 'Վերջանում է',
addToCart: 'Ավելացնել զամբյուղ',
description: 'Նկարագրություն',
reviews: 'Կարծիքներ',
yourReview: 'Ձեր կարծիքը',
leaveReview: 'Թողնել կարծիք',
rating: 'Գնահատական՝',
reviewPlaceholder: 'Կիսվեք ձեր տպավորություններով ապրանքի մասին...',
reviewPlaceholderDexar: 'Կիսվեք ձեր տպավորություններով...',
anonymous: 'Անանուն',
submitting: 'Ուղարկվում է...',
submit: 'Ուղարկել',
reviewSuccess: 'Շնորհակալություն ձեր կարծիքի համար։',
reviewError: 'Ուղարկման սխալ։ Փորձեք ավելի ուշ։',
defaultUser: 'Օգտատեր',
defaultUserDexar: 'Անանուն',
noReviews: 'Դեռ կարծիքներ չկան։ Դարձեք առաջինը։',
qna: 'Հարցեր և պատասխաններ',
photo: 'Լուսանկար',
reviewsCount: 'կարծիք',
today: 'Այսօր',
yesterday: 'Երեկ',
daysAgo: 'օր առաջ',
weeksAgo: 'շաբաթ առաջ',
},
app: {
connecting: 'Միացում սերվերին...',
serverUnavailable: 'Սերվերը հասանելի չէ',
serverError: 'Չհաջողվեց միանալ սերվերին։ Ստուգեք ինտերնետ կապը։',
retryConnection: 'Կրկնել փորձը',
pageTitle: 'Ապրանքների և ծառայությունների մարքեթփլեյս',
},
carousel: {
loading: 'Ապրանքները բեռնվում են...',
addToCart: 'Ավելացնել զամբյուղ',
},
common: {
retry: 'Փորձել կրկին',
loading: 'Բեռնվում է...',
},
};

188
src/app/i18n/ru.ts Normal file
View File

@@ -0,0 +1,188 @@
import { Translations } from './translations';
export const ru: Translations = {
header: {
home: 'Главная',
search: 'Поиск',
about: 'О нас',
contacts: 'Контакты',
searchPlaceholder: 'Искать...',
catalog: 'Каталог',
},
footer: {
description: 'Современный маркетплейс для комфортных покупок',
company: 'Компания',
aboutUs: 'О нас',
contacts: 'Контакты',
requisites: 'Реквизиты',
support: 'Поддержка',
faq: 'FAQ',
delivery: 'Доставка',
guarantee: 'Гарантия',
legal: 'Правовая информация',
offer: 'Оферта',
privacy: 'Конфиденциальность',
returns: 'Возврат',
info: 'Информация',
aboutCompany: 'О компании',
documents: 'Документы',
paymentRules: 'Правила оплаты',
returnPolicy: 'Политика возврата',
publicOffer: 'Публичная оферта',
help: 'Помощь',
payment: 'Оплата',
allRightsReserved: 'Все права защищены.',
},
home: {
welcomeTo: 'Добро пожаловать в {{brand}}',
subtitle: 'Найдите всё, что нужно, в одном месте',
startSearch: 'Начать поиск',
loading: 'Загружаем категории...',
errorTitle: 'Что-то пошло не так',
retry: 'Попробовать снова',
categoriesTitle: 'Категории товаров',
categoriesSubtitle: 'Выберите интересующую категорию',
categoriesEmpty: 'Категории скоро появятся',
categoriesEmptyDesc: 'Мы работаем над наполнением каталога',
dexarHeroTitle: 'Здесь ты найдёшь всё',
dexarHeroSubtitle: 'Тысячи товаров в одном месте',
dexarHeroTagline: 'просто и удобно',
goToCatalog: 'Перейти в каталог',
findProduct: 'Найти товар',
loadingDexar: 'Загрузка категорий...',
catalogTitle: 'Каталог товаров',
emptyCategoriesDexar: 'Категории пока отсутствуют',
categoriesSoonDexar: 'Скоро здесь появятся категории товаров',
itemsCount: '{{count}} товаров',
},
cart: {
title: 'Корзина',
clear: 'Очистить',
empty: 'Корзина пуста',
emptyDesc: 'Добавьте товары, чтобы начать покупки',
goShopping: 'Перейти к покупкам',
total: 'Итого',
items: 'Товары',
deliveryLabel: 'Доставка',
toPay: 'К оплате',
agreeWith: 'Я согласен с',
publicOffer: 'публичной офертой',
returnPolicy: 'политикой возврата',
guaranteeTerms: 'условиями гарантии',
privacyPolicy: 'политикой конфиденциальности',
and: 'и',
checkout: 'Оформить заказ',
close: 'Закрыть',
creatingPayment: 'Создание платежа...',
waitFewSeconds: 'Подождите несколько секунд',
scanQr: 'Сканируйте QR-код для оплаты',
amountToPay: 'Сумма к оплате:',
waitingPayment: 'Ожидание оплаты...',
copied: '✓ Скопировано',
copyLink: 'Скопировать ссылку',
openNewTab: 'Открыть в новой вкладке',
paymentSuccess: 'Поздравляем! Оплата прошла успешно!',
paymentSuccessDesc: 'Введите ваши контактные данные, и мы отправим вам покупку в течение нескольких минут',
sending: 'Отправка...',
send: 'Отправить',
paymentTimeout: 'Время ожидания истекло',
paymentTimeoutDesc: 'Мы не получили подтверждение оплаты в течение 3 минут.',
autoClose: 'Окно закроется автоматически...',
confirmClear: 'Вы уверены, что хотите очистить корзину?',
acceptTerms: 'Пожалуйста, примите условия оферты, политику возврата и возврата для подтверждения оформления заказа.',
copyError: 'Ошибка копирования:',
emailSuccess: 'Email успешно отправлен! Проверьте свою почту.',
emailError: 'Произошла ошибка при отправке email. Пожалуйста, попробуйте снова.',
phoneRequired: 'Номер телефона обязателен',
phoneMoreDigits: 'Введите ещё {{count}} цифр',
phoneTooMany: 'Слишком много цифр',
emailRequired: 'Email обязателен',
emailTooShort: 'Email слишком короткий (минимум 5 символов)',
emailTooLong: 'Email слишком длинный (максимум 100 символов)',
emailNeedsAt: 'Email должен содержать @',
emailNeedsDomain: 'Email должен содержать домен (.com, .ru и т.д.)',
emailInvalid: 'Некорректный формат email',
},
search: {
title: 'Поиск товаров',
placeholder: 'Введите название товара...',
resultsCount: 'Найдено товаров:',
searching: 'Поиск...',
retry: 'Попробовать снова',
noResults: 'Ничего не найдено',
noResultsFor: 'По запросу "{{query}}" товары не найдены',
noResultsHint: 'Попробуйте изменить запрос или используйте другие ключевые слова',
addToCart: 'В корзину',
loadingMore: 'Загрузка...',
allLoaded: 'Все результаты загружены',
emptyState: 'Введите запрос для поиска товаров',
of: 'из',
},
category: {
retry: 'Попробовать снова',
addToCart: 'В корзину',
loadingMore: 'Загрузка...',
allLoaded: 'Все товары загружены',
emptyTitle: 'Упс! Здесь пока пусто',
emptyDesc: 'В этой категории ещё нет товаров, но скоро они появятся',
goHome: 'На главную',
loading: 'Загрузка товаров...',
},
subcategories: {
loading: 'Загрузка подкатегорий...',
retry: 'Попробовать снова',
emptyTitle: 'Упс! Подкатегорий пока нет',
emptyDesc: 'В этом разделе ещё нет подкатегорий, но скоро они появятся',
goHome: 'На главную',
},
itemDetail: {
loading: 'Загрузка...',
loadingDexar: 'Загрузка товара...',
back: 'Вернуться',
backHome: 'Вернуться на главную',
noImage: 'Нет изображения',
stock: 'Наличие:',
inStock: 'В наличии',
lowStock: 'Осталось немного',
lastItems: 'Последние штуки',
mediumStock: 'Заканчивается',
addToCart: 'Добавить в корзину',
description: 'Описание',
reviews: 'Отзывы',
yourReview: 'Ваш отзыв',
leaveReview: 'Оставить отзыв',
rating: 'Оценка:',
reviewPlaceholder: 'Поделитесь своими впечатлениями о товаре...',
reviewPlaceholderDexar: 'Поделитесь впечатлениями о товаре...',
anonymous: 'Анонимно',
submitting: 'Отправка...',
submit: 'Отправить',
reviewSuccess: 'Спасибо за ваш отзыв!',
reviewError: 'Ошибка отправки. Попробуйте позже.',
defaultUser: 'Пользователь',
defaultUserDexar: 'Аноним',
noReviews: 'Пока нет отзывов. Станьте первым!',
qna: 'Вопросы и ответы',
photo: 'Фото',
reviewsCount: 'отзывов',
today: 'Сегодня',
yesterday: 'Вчера',
daysAgo: 'дн. назад',
weeksAgo: 'нед. назад',
},
app: {
connecting: 'Подключение к серверу...',
serverUnavailable: 'Сервер недоступен',
serverError: 'Не удалось подключиться к серверу. Проверьте подключение к интернету.',
retryConnection: 'Повторить попытку',
pageTitle: 'Маркетплейс товаров и услуг',
},
carousel: {
loading: 'Загрузка товаров...',
addToCart: 'Добавить в корзину',
},
common: {
retry: 'Попробовать снова',
loading: 'Загрузка...',
},
};

View File

@@ -0,0 +1,14 @@
import { Pipe, PipeTransform, inject } from '@angular/core';
import { TranslateService } from './translate.service';
@Pipe({
name: 'translate',
pure: false,
})
export class TranslatePipe implements PipeTransform {
private translateService = inject(TranslateService);
transform(key: string, params?: Record<string, string | number>): string {
return this.translateService.t(key, params);
}
}

View File

@@ -0,0 +1,48 @@
import { Injectable, computed, inject } from '@angular/core';
import { LanguageService } from '../services/language.service';
import { Translations } from './translations';
import { ru } from './ru';
import { en } from './en';
import { hy } from './hy';
const translationMap: Record<string, Translations> = { ru, en, hy };
@Injectable({
providedIn: 'root',
})
export class TranslateService {
private langService = inject(LanguageService);
readonly translations = computed<Translations>(
() => translationMap[this.langService.currentLanguage()] ?? ru,
);
/**
* Translate a dot-separated key with optional interpolation params.
* Usage: t('cart.phoneMoreDigits', { count: 3 }) → "Введите ещё 3 цифр"
*/
t(key: string, params?: Record<string, string | number>): string {
const parts = key.split('.');
let result: unknown = this.translations();
for (const part of parts) {
if (result && typeof result === 'object') {
result = (result as Record<string, unknown>)[part];
} else {
return key;
}
}
if (typeof result !== 'string') {
return key;
}
if (params) {
return result.replace(/\{\{(\w+)\}\}/g, (_, k) =>
params[k] !== undefined ? String(params[k]) : `{{${k}}}`,
);
}
return result;
}
}

View File

@@ -0,0 +1,186 @@
export interface Translations {
header: {
home: string;
search: string;
about: string;
contacts: string;
searchPlaceholder: string;
catalog: string;
};
footer: {
description: string;
company: string;
aboutUs: string;
contacts: string;
requisites: string;
support: string;
faq: string;
delivery: string;
guarantee: string;
legal: string;
offer: string;
privacy: string;
returns: string;
info: string;
aboutCompany: string;
documents: string;
paymentRules: string;
returnPolicy: string;
publicOffer: string;
help: string;
payment: string;
allRightsReserved: string;
};
home: {
welcomeTo: string;
subtitle: string;
startSearch: string;
loading: string;
errorTitle: string;
retry: string;
categoriesTitle: string;
categoriesSubtitle: string;
categoriesEmpty: string;
categoriesEmptyDesc: string;
dexarHeroTitle: string;
dexarHeroSubtitle: string;
dexarHeroTagline: string;
goToCatalog: string;
findProduct: string;
loadingDexar: string;
catalogTitle: string;
emptyCategoriesDexar: string;
categoriesSoonDexar: string;
itemsCount: string;
};
cart: {
title: string;
clear: string;
empty: string;
emptyDesc: string;
goShopping: string;
total: string;
items: string;
deliveryLabel: string;
toPay: string;
agreeWith: string;
publicOffer: string;
returnPolicy: string;
guaranteeTerms: string;
privacyPolicy: string;
and: string;
checkout: string;
close: string;
creatingPayment: string;
waitFewSeconds: string;
scanQr: string;
amountToPay: string;
waitingPayment: string;
copied: string;
copyLink: string;
openNewTab: string;
paymentSuccess: string;
paymentSuccessDesc: string;
sending: string;
send: string;
paymentTimeout: string;
paymentTimeoutDesc: string;
autoClose: string;
confirmClear: string;
acceptTerms: string;
copyError: string;
emailSuccess: string;
emailError: string;
phoneRequired: string;
phoneMoreDigits: string;
phoneTooMany: string;
emailRequired: string;
emailTooShort: string;
emailTooLong: string;
emailNeedsAt: string;
emailNeedsDomain: string;
emailInvalid: string;
};
search: {
title: string;
placeholder: string;
resultsCount: string;
searching: string;
retry: string;
noResults: string;
noResultsFor: string;
noResultsHint: string;
addToCart: string;
loadingMore: string;
allLoaded: string;
emptyState: string;
of: string;
};
category: {
retry: string;
addToCart: string;
loadingMore: string;
allLoaded: string;
emptyTitle: string;
emptyDesc: string;
goHome: string;
loading: string;
};
subcategories: {
loading: string;
retry: string;
emptyTitle: string;
emptyDesc: string;
goHome: string;
};
itemDetail: {
loading: string;
loadingDexar: string;
back: string;
backHome: string;
noImage: string;
stock: string;
inStock: string;
lowStock: string;
lastItems: string;
mediumStock: string;
addToCart: string;
description: string;
reviews: string;
yourReview: string;
leaveReview: string;
rating: string;
reviewPlaceholder: string;
reviewPlaceholderDexar: string;
anonymous: string;
submitting: string;
submit: string;
reviewSuccess: string;
reviewError: string;
defaultUser: string;
defaultUserDexar: string;
noReviews: string;
qna: string;
photo: string;
reviewsCount: string;
today: string;
yesterday: string;
daysAgo: string;
weeksAgo: string;
};
app: {
connecting: string;
serverUnavailable: string;
serverError: string;
retryConnection: string;
pageTitle: string;
};
carousel: {
loading: string;
addToCart: string;
};
common: {
retry: string;
loading: string;
};
}

View File

@@ -2,7 +2,7 @@ import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
const cache = new Map<string, { response: HttpResponse<any>, timestamp: number }>(); const cache = new Map<string, { response: HttpResponse<unknown>, timestamp: number }>();
const CACHE_DURATION = 5 * 60 * 1000; // 5 минут const CACHE_DURATION = 5 * 60 * 1000; // 5 минут
export const cacheInterceptor: HttpInterceptorFn = (req, next) => { export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
@@ -17,6 +17,9 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
return next(req); return next(req);
} }
// Cleanup expired entries before checking
cleanupExpiredCache();
const cachedResponse = cache.get(req.url); const cachedResponse = cache.get(req.url);
// Проверяем наличие и актуальность кэша // Проверяем наличие и актуальность кэша
@@ -25,7 +28,6 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
if (age < CACHE_DURATION) { if (age < CACHE_DURATION) {
return of(cachedResponse.response.clone()); return of(cachedResponse.response.clone());
} else { } else {
// Кэш устарел, удаляем
cache.delete(req.url); cache.delete(req.url);
} }
} }
@@ -43,19 +45,16 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
); );
}; };
// Функция для очистки кэша (можно использовать при необходимости) /** Clear all cached responses */
export function clearCache(): void { export function clearCache(): void {
cache.clear(); cache.clear();
// console.log('[Cache] Cache cleared');
} }
// Функция для очистки устаревшего кэша function cleanupExpiredCache(): void {
export function cleanupExpiredCache(): void {
const now = Date.now(); const now = Date.now();
for (const [url, data] of cache.entries()) { for (const [url, data] of cache.entries()) {
if (now - data.timestamp >= CACHE_DURATION) { if (now - data.timestamp >= CACHE_DURATION) {
cache.delete(url); cache.delete(url);
} }
} }
// console.log('[Cache] Expired cache cleaned up');
} }

View File

@@ -3,4 +3,7 @@ export interface Category {
name: string; name: string;
parentID: number; parentID: number;
icon?: string; icon?: string;
wideBanner?: string;
itemCount?: number;
priority?: number;
} }

View File

@@ -5,7 +5,7 @@ export interface Photo {
type?: string; type?: string;
} }
export interface Callback { export interface Review {
rating?: number; rating?: number;
content?: string; content?: string;
userID?: string; userID?: string;
@@ -13,6 +13,9 @@ export interface Callback {
timestamp?: string; timestamp?: string;
} }
/** @deprecated Use {@link Review} instead */
export type Callback = Review;
export interface Question { export interface Question {
question: string; question: string;
answer: string; answer: string;
@@ -31,10 +34,9 @@ export interface Item {
discount: number; discount: number;
remainings?: string; remainings?: string;
rating: number; rating: number;
callbacks: Callback[] | null; callbacks: Review[] | null;
questions: Question[] | null; questions: Question[] | null;
partnerID?: string; partnerID?: string;
quantity?: number; // For cart items
} }
export interface CartItem extends Item { export interface CartItem extends Item {

View File

@@ -1,12 +1,12 @@
<div [class]="isnovo ? 'cart-container novo' : 'cart-container dexar'"> <div [class]="isnovo ? 'cart-container novo' : 'cart-container dexar'">
<div class="cart-header"> <div class="cart-header">
<h1>Корзина</h1> <h1>{{ 'cart.title' | translate }}</h1>
@if (itemCount() > 0) { @if (itemCount() > 0) {
<button class="clear-cart-btn" (click)="clearCart()"> <button class="clear-cart-btn" (click)="clearCart()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/> <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
</svg> </svg>
Очистить {{ 'cart.clear' | translate }}
</button> </button>
} }
</div> </div>
@@ -16,9 +16,9 @@
<div class="empty-icon"> <div class="empty-icon">
<app-empty-cart-icon /> <app-empty-cart-icon />
</div> </div>
<h2>Корзина пуста</h2> <h2>{{ 'cart.empty' | translate }}</h2>
<p>Добавьте товары, чтобы начать покупки</p> <p>{{ 'cart.emptyDesc' | translate }}</p>
<a routerLink="/" class="shop-btn">Перейти к покупкам</a> <a [routerLink]="'/' | langRoute" class="shop-btn">{{ 'cart.goShopping' | translate }}</a>
</div> </div>
} }
@@ -30,13 +30,13 @@
[class.swiped]="swipedItemId() === item.itemID" [class.swiped]="swipedItemId() === item.itemID"
(touchstart)="onSwipeStart(item.itemID, $event)"> (touchstart)="onSwipeStart(item.itemID, $event)">
<div class="cart-item"> <div class="cart-item">
<a [routerLink]="['/item', item.itemID]" class="item-image"> <a [routerLink]="['/item', item.itemID] | langRoute" class="item-image">
<img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" /> <img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" />
</a> </a>
<div class="item-info"> <div class="item-info">
<div class="item-header"> <div class="item-header">
<a [routerLink]="['/item', item.itemID]" class="item-name">{{ item.name }}</a> <a [routerLink]="['/item', item.itemID] | langRoute" class="item-name">{{ item.name }}</a>
<button class="remove-btn" (click)="removeItem(item.itemID)" title="Remove"> <button class="remove-btn" (click)="removeItem(item.itemID)" title="Remove">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/> <path d="M18 6L6 18M6 6l12 12"/>
@@ -86,21 +86,21 @@
<div class="cart-summary"> <div class="cart-summary">
<div class="summary-header"> <div class="summary-header">
<h3>Итого</h3> <h3>{{ 'cart.total' | translate }}</h3>
</div> </div>
<div class="summary-row"> <div class="summary-row">
<span>Товары ({{ itemCount() }})</span> <span>{{ 'cart.items' | translate }} ({{ itemCount() }})</span>
<span class="value">{{ totalPrice() | number:'1.2-2' }} ₽</span> <span class="value">{{ totalPrice() | number:'1.2-2' }} ₽</span>
</div> </div>
<div class="summary-row delivery"> <div class="summary-row delivery">
<span>Доставка</span> <span>{{ 'cart.deliveryLabel' | translate }}</span>
<span>0 ₽</span> <span>0 ₽</span>
</div> </div>
<div class="summary-row total"> <div class="summary-row total">
<span>К оплате</span> <span>{{ 'cart.toPay' | translate }}</span>
<span class="total-price">{{ totalPrice() | number:'1.2-2' }} ₽</span> <span class="total-price">{{ totalPrice() | number:'1.2-2' }} ₽</span>
</div> </div>
@@ -113,11 +113,11 @@
/> />
<span class="checkmark"></span> <span class="checkmark"></span>
<span class="terms-text"> <span class="terms-text">
Я согласен с {{ 'cart.agreeWith' | translate }}
<a routerLink="/public-offer" target="_blank">публичной офертой</a>, <a [routerLink]="'/public-offer' | langRoute" target="_blank">{{ 'cart.publicOffer' | translate }}</a>,
<a routerLink="/return-policy" target="_blank">политикой возврата</a>, <a [routerLink]="'/return-policy' | langRoute" target="_blank">{{ 'cart.returnPolicy' | translate }}</a>,
<a routerLink="/guarantee" target="_blank">условиями гарантии</a> и <a [routerLink]="'/guarantee' | langRoute" target="_blank">{{ 'cart.guaranteeTerms' | translate }}</a> {{ 'cart.and' | translate }}
<a routerLink="/privacy-policy" target="_blank">политикой конфиденциальности</a> <a [routerLink]="'/privacy-policy' | langRoute" target="_blank">{{ 'cart.privacyPolicy' | translate }}</a>
</span> </span>
</label> </label>
</div> </div>
@@ -128,7 +128,7 @@
[class.disabled]="!termsAccepted" [class.disabled]="!termsAccepted"
[disabled]="!termsAccepted" [disabled]="!termsAccepted"
> >
Оформить заказ {{ 'cart.checkout' | translate }}
</button> </button>
</div> </div>
</div> </div>
@@ -139,7 +139,7 @@
@if (showPaymentPopup()) { @if (showPaymentPopup()) {
<div class="payment-modal-overlay"> <div class="payment-modal-overlay">
<div class="payment-modal"> <div class="payment-modal">
<button class="close-modal-btn" (click)="closePaymentPopup()" aria-label="Закрыть"> <button class="close-modal-btn" (click)="closePaymentPopup()" [attr.aria-label]="'cart.close' | translate">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg> </svg>
@@ -147,14 +147,14 @@
@if (paymentStatus() === 'creating') { @if (paymentStatus() === 'creating') {
<div class="payment-status-screen"> <div class="payment-status-screen">
<div class="spinner-large"></div> <div class="spinner-large"></div>
<h2>Создание платежа...</h2> <h2>{{ 'cart.creatingPayment' | translate }}</h2>
<p>Подождите несколько секунд</p> <p>{{ 'cart.waitFewSeconds' | translate }}</p>
</div> </div>
} }
@if (paymentStatus() === 'waiting') { @if (paymentStatus() === 'waiting') {
<div class="payment-active"> <div class="payment-active">
<h2>Сканируйте QR-код для оплаты</h2> <h2>{{ 'cart.scanQr' | translate }}</h2>
<div class="qr-section"> <div class="qr-section">
<div class="qr-wrapper"> <div class="qr-wrapper">
@@ -165,22 +165,22 @@
<div class="payment-info"> <div class="payment-info">
<div class="payment-amount"> <div class="payment-amount">
<span class="label">Сумма к оплате:</span> <span class="label">{{ 'cart.amountToPay' | translate }}</span>
<span class="amount">{{ totalPrice() | number:'1.2-2' }} RUB</span> <span class="amount">{{ totalPrice() | number:'1.2-2' }} RUB</span>
</div> </div>
<div class="waiting-indicator"> <div class="waiting-indicator">
<div class="pulse-dot"></div> <div class="pulse-dot"></div>
<span>Ожидание оплаты...</span> <span>{{ 'cart.waitingPayment' | translate }}</span>
</div> </div>
</div> </div>
<div class="payment-actions"> <div class="payment-actions">
<button class="copy-btn" (click)="copyPaymentLink()"> <button class="copy-btn" (click)="copyPaymentLink()">
{{ linkCopied() ? '✓ Скопировано' : 'Скопировать ссылку' }} {{ linkCopied() ? ('cart.copied' | translate) : ('cart.copyLink' | translate) }}
</button> </button>
<a [href]="paymentUrl()" target="_blank" rel="noopener noreferrer" class="open-btn"> <a [href]="paymentUrl()" target="_blank" rel="noopener noreferrer" class="open-btn">
Открыть в новой вкладке {{ 'cart.openNewTab' | translate }}
</a> </a>
</div> </div>
</div> </div>
@@ -189,8 +189,8 @@
@if (paymentStatus() === 'success') { @if (paymentStatus() === 'success') {
<div class="payment-status-screen success"> <div class="payment-status-screen success">
<div class="success-icon"></div> <div class="success-icon"></div>
<h2>Поздравляем! Оплата прошла успешно!</h2> <h2>{{ 'cart.paymentSuccess' | translate }}</h2>
<p class="success-text">Введите ваши контактные данные, и мы отправим вам покупку в течение нескольких минут</p> <p class="success-text">{{ 'cart.paymentSuccessDesc' | translate }}</p>
<div class="email-form"> <div class="email-form">
<div class="input-group"> <div class="input-group">
@@ -236,9 +236,9 @@
> >
@if (emailSubmitting()) { @if (emailSubmitting()) {
<span class="spinner-small"></span> <span class="spinner-small"></span>
Отправка... {{ 'cart.sending' | translate }}
} @else { } @else {
Отправить {{ 'cart.send' | translate }}
} }
</button> </button>
</div> </div>
@@ -248,9 +248,9 @@
@if (paymentStatus() === 'timeout') { @if (paymentStatus() === 'timeout') {
<div class="payment-status-screen timeout"> <div class="payment-status-screen timeout">
<div class="timeout-icon"></div> <div class="timeout-icon"></div>
<h2>Время ожидания истекло</h2> <h2>{{ 'cart.paymentTimeout' | translate }}</h2>
<p>Мы не получили подтверждение оплаты в течение 3 минут.</p> <p>{{ 'cart.paymentTimeoutDesc' | translate }}</p>
<p class="auto-close">Окно закроется автоматически...</p> <p class="auto-close">{{ 'cart.autoClose' | translate }}</p>
</div> </div>
} }
</div> </div>

View File

@@ -2,8 +2,9 @@
.cart-container.dexar { .cart-container.dexar {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 24px;
min-height: calc(100vh - 200px); min-height: calc(100vh - 200px);
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
} }
// Novo theme - Modern green design // Novo theme - Modern green design
@@ -16,38 +17,44 @@
} }
.cart-container.dexar .cart-header { .cart-container.dexar .cart-header {
margin-bottom: 24px; margin-bottom: 28px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
h1 { h1 {
font-size: 1.75rem; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600; font-size: 2rem;
color: #1a1a1a; font-weight: 700;
color: #1e3c38;
margin: 0; margin: 0;
} }
.clear-cart-btn { .clear-cart-btn {
padding: 8px 16px; padding: 10px 20px;
background: #ef4444; background: transparent;
color: white; color: #697777;
border: none; border: 1px solid #d3dad9;
border-radius: 8px; border-radius: 13px;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 500; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
svg { svg {
display: none; display: block;
width: 16px;
height: 16px;
} }
&:hover { &:hover {
background: #dc2626; background: #fef2f2;
border-color: #ef4444;
color: #ef4444;
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -114,41 +121,51 @@
justify-content: center; justify-content: center;
padding: 80px 20px; padding: 80px 20px;
text-align: center; text-align: center;
background: #f5f3f9;
border-radius: 13px;
border: 1px solid #d3dad9;
.empty-icon { .empty-icon {
margin-bottom: 24px; margin-bottom: 28px;
svg { svg {
width: 80px; width: 90px;
height: 80px; height: 90px;
opacity: 0.7;
} }
} }
h2 { h2 {
font-size: 1.5rem; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600; font-size: 1.6rem;
color: #1a1a1a; font-weight: 700;
color: #1e3c38;
margin: 0 0 12px 0; margin: 0 0 12px 0;
} }
p { p {
font-size: 0.95rem; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #6b7280; font-size: 1rem;
margin: 0 0 24px 0; color: #697777;
margin: 0 0 28px 0;
} }
.shop-btn { .shop-btn {
padding: 12px 24px; padding: 14px 32px;
background: #a855f7; background: #497671;
color: white; color: white;
text-decoration: none; text-decoration: none;
border-radius: 8px; border-radius: 13px;
font-weight: 500; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600;
font-size: 1rem;
transition: all 0.2s; transition: all 0.2s;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
&:hover { &:hover {
background: #9333ea; background: #3a5f5b;
transform: translateY(-1px); transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(73, 118, 113, 0.3);
} }
} }
} }
@@ -256,26 +273,30 @@
// Dexar cart item // Dexar cart item
.cart-container.dexar .cart-item { .cart-container.dexar .cart-item {
display: flex; display: flex;
gap: 16px; gap: 20px;
background: white; background: white;
border: 1px solid #e5e7eb; border: 1px solid #d3dad9;
border-radius: 12px; border-radius: 13px;
padding: 16px; padding: 20px;
transition: all 0.3s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
z-index: 1; z-index: 1;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
&:hover { &:hover {
border-color: #d1d5db; border-color: #a1b4b5;
box-shadow: 0 4px 12px rgba(73, 118, 113, 0.1);
transform: translateY(-1px);
} }
.item-image { .item-image {
flex-shrink: 0; flex-shrink: 0;
width: 80px; width: 100px;
height: 80px; height: 100px;
border-radius: 8px; border-radius: 13px;
overflow: hidden; overflow: hidden;
background: #f3f4f6; background: #f5f3f9;
border: 1px solid #d3dad9;
img { img {
width: 100%; width: 100%;
@@ -283,6 +304,12 @@
object-fit: contain; object-fit: contain;
background: white; background: white;
display: block; display: block;
padding: 6px;
transition: transform 0.3s ease;
}
&:hover img {
transform: scale(1.05);
} }
} }
@@ -302,25 +329,26 @@
} }
.item-name { .item-name {
font-size: 1rem; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 1.05rem;
font-weight: 600; font-weight: 600;
color: #1a1a1a; color: #1e3c38;
text-decoration: none; text-decoration: none;
transition: color 0.2s; transition: color 0.2s;
&:hover { &:hover {
color: #a855f7; color: #497671;
} }
} }
.remove-btn { .remove-btn {
flex-shrink: 0; flex-shrink: 0;
padding: 4px; padding: 6px;
background: transparent; background: transparent;
border: none; border: none;
color: #9ca3af; color: #a1b4b5;
cursor: pointer; cursor: pointer;
border-radius: 6px; border-radius: 8px;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
@@ -330,8 +358,9 @@
} }
.item-description { .item-description {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 0.875rem; font-size: 0.875rem;
color: #6b7280; color: #697777;
line-height: 1.5; line-height: 1.5;
} }
@@ -462,9 +491,10 @@
} }
.current-price { .current-price {
font-size: 1.1rem; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 1.15rem;
font-weight: 700; font-weight: 700;
color: #1a1a1a; color: #497671;
} }
} }
@@ -473,16 +503,17 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
background: #f3f4f6; background: #f5f3f9;
border-radius: 20px; border-radius: 13px;
padding: 4px 8px; padding: 4px 10px;
border: 1px solid #d3dad9;
.qty-btn { .qty-btn {
width: 28px; width: 30px;
height: 28px; height: 30px;
border: none; border: none;
background: transparent; background: transparent;
color: #a855f7; color: #497671;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -491,7 +522,7 @@
transition: all 0.2s; transition: all 0.2s;
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: #a855f7; background: #497671;
color: white; color: white;
} }
@@ -502,10 +533,11 @@
} }
.qty-value { .qty-value {
font-size: 0.95rem; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600; font-size: 1rem;
color: #1a1a1a; font-weight: 700;
min-width: 20px; color: #1e3c38;
min-width: 24px;
text-align: center; text-align: center;
} }
} }
@@ -582,19 +614,21 @@
// Dexar Cart Summary // Dexar Cart Summary
.cart-container.dexar .cart-summary { .cart-container.dexar .cart-summary {
position: sticky; position: sticky;
top: 20px; top: 90px;
background: white; background: #f5f3f9;
border: 1px solid #e5e7eb; border: 1px solid #d3dad9;
border-radius: 16px; border-radius: 13px;
padding: 24px; padding: 28px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
.summary-header h3 { .summary-header h3 {
font-size: 1.25rem; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600; font-size: 1.4rem;
color: #1a1a1a; font-weight: 700;
color: #1e3c38;
margin: 0 0 16px 0; margin: 0 0 16px 0;
} }
@@ -602,8 +636,9 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 0.95rem; font-size: 0.95rem;
color: #6b7280; color: #697777;
&.delivery { &.delivery {
display: flex; display: flex;
@@ -611,35 +646,38 @@
&.total { &.total {
padding-top: 16px; padding-top: 16px;
border-top: 1px solid #e5e7eb; border-top: 1px solid #d3dad9;
font-size: 1.1rem; font-size: 1.15rem;
font-weight: 600; font-weight: 700;
color: #1a1a1a; color: #1e3c38;
margin-top: 8px; margin-top: 8px;
.total-price { .total-price {
font-size: 1.25rem; font-size: 1.35rem;
color: #1a1a1a; color: #497671;
} }
} }
} }
.checkout-btn { .checkout-btn {
width: 100%; width: 100%;
padding: 14px; padding: 16px;
background: linear-gradient(135deg, #a855f7 0%, #9333ea 100%); background: #497671;
color: white; color: white;
border: none; border: none;
border-radius: 12px; border-radius: 13px;
font-size: 1rem; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600; font-size: 1.05rem;
font-weight: 700;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
margin-top: 8px; margin-top: 8px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
&:hover:not(:disabled) { &:hover:not(:disabled) {
transform: translateY(-1px); background: #3a5f5b;
box-shadow: 0 8px 16px rgba(168, 85, 247, 0.3); transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(73, 118, 113, 0.3);
} }
&:active { &:active {
@@ -790,7 +828,7 @@
line-height: 1.4; line-height: 1.4;
a { a {
color: #a855f7; color: var(--primary-color);
text-decoration: none; text-decoration: none;
&:hover { &:hover {
@@ -804,13 +842,36 @@
// Dexar checkbox colors // Dexar checkbox colors
.cart-container.dexar .terms-agreement .checkbox-container { .cart-container.dexar .terms-agreement .checkbox-container {
input[type="checkbox"]:checked ~ .checkmark { input[type="checkbox"]:checked ~ .checkmark {
background: #a855f7; background: #497671;
border-color: #a855f7; border-color: #497671;
&::after { &::after {
display: block; display: block;
} }
} }
.checkmark {
border: 2px solid #d3dad9;
border-radius: 4px;
}
.terms-text {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 0.85rem;
color: #697777;
line-height: 1.5;
a {
color: #497671;
text-decoration: none;
font-weight: 600;
&:hover {
text-decoration: underline;
color: #3a5f5b;
}
}
}
} }
// Novo checkbox colors - green // Novo checkbox colors - green
@@ -861,13 +922,15 @@
.payment-modal { .payment-modal {
background: white; background: white;
border-radius: 20px; border-radius: 13px;
max-width: 500px; max-width: 500px;
width: 100%; width: 100%;
padding: 40px; padding: 40px;
position: relative; position: relative;
max-height: 90vh; max-height: 90vh;
overflow-y: auto; overflow-y: auto;
border: 1px solid #d3dad9;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
} }
.close-modal-btn { .close-modal-btn {
position: absolute; position: absolute;
@@ -980,8 +1043,8 @@
.spinner-small { .spinner-small {
width: 50px; width: 50px;
height: 50px; height: 50px;
border: 4px solid #f3f3f3; border: 4px solid #d3dad9;
border-top: 4px solid #a855f7; border-top: 4px solid #497671;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto; margin: 0 auto;
@@ -1032,9 +1095,9 @@
left: 16px; left: 16px;
right: 16px; right: 16px;
height: 2px; height: 2px;
background: linear-gradient(90deg, transparent, #a855f7, transparent); background: linear-gradient(90deg, transparent, #497671, transparent);
animation: scan 2s linear infinite; animation: scan 2s linear infinite;
box-shadow: 0 0 10px #a855f7; box-shadow: 0 0 10px #497671;
} }
@keyframes scan { @keyframes scan {
@@ -1084,7 +1147,7 @@
.pulse-dot { .pulse-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
background: #a855f7; background: #497671;
border-radius: 50%; border-radius: 50%;
animation: pulse 1.5s ease-in-out infinite; animation: pulse 1.5s ease-in-out infinite;
} }
@@ -1132,12 +1195,12 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.25); box-shadow: 0 4px 12px rgba(73, 118, 113, 0.25);
&:hover { &:hover {
background: var(--primary-hover); background: var(--primary-hover);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.35); box-shadow: 0 6px 16px rgba(73, 118, 113, 0.35);
} }
&:active { &:active {
@@ -1198,8 +1261,8 @@
transition: all 0.2s; transition: all 0.2s;
&:focus { &:focus {
border-color: #a855f7; border-color: #497671;
box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.1); box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.1);
} }
&.valid { &.valid {
@@ -1225,12 +1288,13 @@
.submit-email-btn { .submit-email-btn {
padding: 14px; padding: 14px;
background: linear-gradient(135deg, #a855f7 0%, #9333ea 100%); background: #497671;
color: white; color: white;
border: none; border: none;
border-radius: 8px; border-radius: 13px;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 700;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1239,8 +1303,9 @@
transition: all 0.2s; transition: all 0.2s;
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: #3a5f5b;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(168, 85, 247, 0.3); box-shadow: 0 4px 12px rgba(73, 118, 113, 0.3);
} }
&:disabled { &:disabled {

View File

@@ -1,29 +1,34 @@
import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy, OnInit } from '@angular/core'; import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { DecimalPipe } from '@angular/common';
import { Router, RouterLink } from '@angular/router'; import { Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { CartService, ApiService } from '../../services'; import { CartService, ApiService, LanguageService } from '../../services';
import { Item, CartItem } from '../../models'; import { Item, CartItem } from '../../models';
import { interval, Subscription } from 'rxjs'; import { interval, Subscription } from 'rxjs';
import { switchMap, take } from 'rxjs/operators'; import { switchMap, take } from 'rxjs/operators';
import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component'; import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service';
@Component({ @Component({
selector: 'app-cart', selector: 'app-cart',
standalone: true, imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, LangRoutePipe, TranslatePipe],
imports: [CommonModule, RouterLink, FormsModule, EmptyCartIconComponent],
templateUrl: './cart.component.html', templateUrl: './cart.component.html',
styleUrls: ['./cart.component.scss'], styleUrls: ['./cart.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class CartComponent implements OnInit, OnDestroy { export class CartComponent implements OnDestroy {
items; items;
itemCount; itemCount;
totalPrice; totalPrice;
termsAccepted = false; termsAccepted = false;
isnovo = environment.theme === 'novo'; isnovo = environment.theme === 'novo';
private i18n = inject(TranslateService);
// Swipe state // Swipe state
swipedItemId = signal<number | null>(null); swipedItemId = signal<number | null>(null);
@@ -52,17 +57,14 @@ export class CartComponent implements OnInit, OnDestroy {
constructor( constructor(
private cartService: CartService, private cartService: CartService,
private apiService: ApiService, private apiService: ApiService,
private router: Router private router: Router,
private langService: LanguageService
) { ) {
this.items = this.cartService.items; this.items = this.cartService.items;
this.itemCount = this.cartService.itemCount; this.itemCount = this.cartService.itemCount;
this.totalPrice = this.cartService.totalPrice; this.totalPrice = this.cartService.totalPrice;
} }
ngOnInit(): void {
// Component initialized
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.stopPolling(); this.stopPolling();
if (this.closeTimeout) { if (this.closeTimeout) {
@@ -109,36 +111,27 @@ export class CartComponent implements OnInit, OnDestroy {
}; };
const cleanup = () => { const cleanup = () => {
document.removeEventListener('touchmove', onMove as any); document.removeEventListener('touchmove', onMove);
document.removeEventListener('touchend', cleanup); document.removeEventListener('touchend', cleanup);
}; };
document.addEventListener('touchmove', onMove as any); document.addEventListener('touchmove', onMove);
document.addEventListener('touchend', cleanup); document.addEventListener('touchend', cleanup);
} }
clearCart(): void { clearCart(): void {
if (confirm('Вы уверены, что хотите очистить корзину?')) { if (confirm(this.i18n.t('cart.confirmClear'))) {
this.cartService.clearCart(); this.cartService.clearCart();
} }
} }
getMainImage(item: Item): string { readonly getMainImage = getMainImage;
return item.photos?.[0]?.url || ''; readonly trackByItemId = trackByItemId;
} readonly getDiscountedPrice = getDiscountedPrice;
// TrackBy function for performance optimization
trackByItemId(index: number, item: Item): number {
return item.itemID;
}
getDiscountedPrice(item: Item): number {
return item.price * (1 - item.discount / 100);
}
checkout(): void { checkout(): void {
if (!this.termsAccepted) { if (!this.termsAccepted) {
alert('Пожалуйста, примите условия договора, политику возврата и гарантии для продолжения оформления заказа.'); alert(this.i18n.t('cart.acceptTerms'));
return; return;
} }
this.openPaymentPopup(); this.openPaymentPopup();
@@ -260,7 +253,7 @@ export class CartComponent implements OnInit, OnDestroy {
this.linkCopied.set(true); this.linkCopied.set(true);
setTimeout(() => this.linkCopied.set(false), 2000); setTimeout(() => this.linkCopied.set(false), 2000);
}).catch(err => { }).catch(err => {
console.error('Ошибка копирования:', err); console.error(this.i18n.t('cart.copyError'), err);
}); });
} }
} }
@@ -325,17 +318,18 @@ export class CartComponent implements OnInit, OnDestroy {
next: () => { next: () => {
this.emailSubmitting.set(false); this.emailSubmitting.set(false);
// Show success message // Show success message
alert('Email успешно отправлен! Проверьте вашу почту.'); alert(this.i18n.t('cart.emailSuccess'));
// Close popup and redirect to home page // Close popup and redirect to home page
setTimeout(() => { setTimeout(() => {
this.closePaymentPopup(); this.closePaymentPopup();
this.router.navigate(['/']); const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]);
}, 500); }, 500);
}, },
error: (err) => { error: (err) => {
console.error('Error submitting email:', err); console.error('Error submitting email:', err);
this.emailSubmitting.set(false); this.emailSubmitting.set(false);
alert('Произошла ошибка при отправке email. Пожалуйста, попробуйте снова.'); alert(this.i18n.t('cart.emailError'));
} }
}); });
} }
@@ -396,11 +390,11 @@ export class CartComponent implements OnInit, OnDestroy {
} }
if (digitsOnly.length === 0) { if (digitsOnly.length === 0) {
this.phoneError.set('Номер телефона обязателен'); this.phoneError.set(this.i18n.t('cart.phoneRequired'));
} else if (digitsOnly.length < 11) { } else if (digitsOnly.length < 11) {
this.phoneError.set(`Введите еще ${11 - digitsOnly.length} цифр`); this.phoneError.set(this.i18n.t('cart.phoneMoreDigits', { count: 11 - digitsOnly.length }));
} else if (digitsOnly.length > 11) { } else if (digitsOnly.length > 11) {
this.phoneError.set('Слишком много цифр'); this.phoneError.set(this.i18n.t('cart.phoneTooMany'));
} else { } else {
this.phoneError.set(''); this.phoneError.set('');
} }
@@ -428,19 +422,19 @@ export class CartComponent implements OnInit, OnDestroy {
} }
if (email.length === 0) { if (email.length === 0) {
this.emailError.set('Email обязателен'); this.emailError.set(this.i18n.t('cart.emailRequired'));
} else if (email.length < 5) { } else if (email.length < 5) {
this.emailError.set('Email слишком короткий (минимум 5 символов)'); this.emailError.set(this.i18n.t('cart.emailTooShort'));
} else if (email.length > 100) { } else if (email.length > 100) {
this.emailError.set('Email слишком длинный (максимум 100 символов)'); this.emailError.set(this.i18n.t('cart.emailTooLong'));
} else if (!email.includes('@')) { } else if (!email.includes('@')) {
this.emailError.set('Email должен содержать @'); this.emailError.set(this.i18n.t('cart.emailNeedsAt'));
} else if (!email.includes('.')) { } else if (!email.includes('.')) {
this.emailError.set('Email должен содержать домен (.com, .ru и т.д.)'); this.emailError.set(this.i18n.t('cart.emailNeedsDomain'));
} else { } else {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) { if (!emailRegex.test(email)) {
this.emailError.set('Некорректный формат email'); this.emailError.set(this.i18n.t('cart.emailInvalid'));
} else { } else {
this.emailError.set(''); this.emailError.set('');
} }

View File

@@ -2,7 +2,7 @@
@if (error()) { @if (error()) {
<div class="error"> <div class="error">
<p>{{ error() }}</p> <p>{{ error() }}</p>
<button (click)="resetAndLoad()">Попробовать снова</button> <button (click)="resetAndLoad()">{{ 'category.retry' | translate }}</button>
</div> </div>
} }
@@ -10,7 +10,7 @@
<div class="items-grid"> <div class="items-grid">
@for (item of items(); track trackByItemId($index, item)) { @for (item of items(); track trackByItemId($index, item)) {
<div class="item-card"> <div class="item-card">
<a [routerLink]="['/item', item.itemID]" class="item-link"> <a [routerLink]="['/item', item.itemID] | langRoute" class="item-link">
<div class="item-image"> <div class="item-image">
<img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" decoding="async" width="300" height="300" /> <img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" decoding="async" width="300" height="300" />
@if (item.discount > 0) { @if (item.discount > 0) {
@@ -46,7 +46,7 @@
</a> </a>
<button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)"> <button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)">
В корзину {{ 'category.addToCart' | translate }}
</button> </button>
</div> </div>
} }
@@ -55,26 +55,36 @@
@if (loading() && items().length > 0) { @if (loading() && items().length > 0) {
<div class="loading-more"> <div class="loading-more">
<div class="spinner"></div> <div class="spinner"></div>
<p>Загрузка...</p> <p>{{ 'category.loadingMore' | translate }}</p>
</div> </div>
} }
@if (!hasMore() && items().length > 0) { @if (!hasMore() && items().length > 0) {
<div class="no-more"> <div class="no-more">
<p>Все товары загружены</p> <p>{{ 'category.allLoaded' | translate }}</p>
</div> </div>
} }
@if (items().length === 0 && !loading()) { @if (items().length === 0 && !loading()) {
<div class="no-items"> <div class="no-items">
<p>В этой категории пока нет товаров</p> <div class="no-items-icon">
<svg width="72" height="72" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 7H4C3.44772 7 3 7.44772 3 8V19C3 19.5523 3.44772 20 4 20H20C20.5523 20 21 19.5523 21 19V8C21 7.44772 20.5523 7 20 7Z" stroke="#a1b4b5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 7V5C16 4.46957 15.7893 3.96086 15.4142 3.58579C15.0391 3.21071 14.5304 3 14 3H10C9.46957 3 8.96086 3.21071 8.58579 3.58579C8.21071 3.96086 8 4.46957 8 5V7" stroke="#a1b4b5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 12V16" stroke="#d3dad9" stroke-width="1.5" stroke-linecap="round"/>
<path d="M10 14H14" stroke="#d3dad9" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<h3>{{ 'category.emptyTitle' | translate }}</h3>
<p>{{ 'category.emptyDesc' | translate }}</p>
<a [routerLink]="'/' | langRoute" class="no-items-btn">{{ 'category.goHome' | translate }}</a>
</div> </div>
} }
@if (loading() && items().length === 0) { @if (loading() && items().length === 0) {
<div class="loading-initial"> <div class="loading-initial">
<div class="spinner"></div> <div class="spinner"></div>
<p>Загрузка товаров...</p> <p>{{ 'category.loading' | translate }}</p>
</div> </div>
} }
} }

View File

@@ -1,12 +1,12 @@
.category-container { .category-container {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 24px;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
} }
.error, .error,
.loading-initial, .loading-initial,
.no-items,
.no-more { .no-more {
text-align: center; text-align: center;
padding: 60px 20px; padding: 60px 20px;
@@ -14,42 +14,109 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #697777;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.no-items {
text-align: center;
padding: 80px 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
.no-items-icon {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f3f9;
border-radius: 50%;
margin-bottom: 24px;
}
h3 {
font-size: 1.25rem;
font-weight: 700;
color: #1e3c38;
margin: 0 0 8px 0;
}
p {
font-size: 0.95rem;
color: #697777;
margin: 0 0 24px 0;
max-width: 340px;
line-height: 1.5;
}
.no-items-btn {
display: inline-flex;
align-items: center;
padding: 10px 28px;
background: #497671;
color: white;
border-radius: 13px;
font-family: "DM Sans", sans-serif;
font-size: 0.9375rem;
font-weight: 600;
text-decoration: none;
transition: background 0.2s ease, transform 0.15s ease;
&:hover {
background: #3d635f;
transform: translateY(-1px);
}
}
} }
.error button { .error button {
margin-top: 20px; margin-top: 20px;
padding: 10px 24px; padding: 10px 24px;
background: var(--primary-color); background: #497671;
color: white; color: white;
border: none; border: none;
border-radius: 6px; border-radius: 8px;
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 500;
transition: all 0.2s ease;
&:hover { &:hover {
background: var(--primary-hover); background: #3a5f5b;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(73, 118, 113, 0.3);
} }
} }
.items-grid { .items-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); grid-template-columns: repeat(4, 1fr);
gap: 24px; gap: 30px;
margin-bottom: 40px; margin-bottom: 40px;
width: 100%;
} }
.item-card { .item-card {
background: white; width: 100%;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover { &:hover {
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
.item-image {
box-shadow: 0 6px 8px 0 rgba(0, 0, 0, 0.2);
}
.item-details {
box-shadow: 0 6px 8px 0 rgba(0, 0, 0, 0.2);
}
} }
} }
@@ -64,18 +131,27 @@
.item-image { .item-image {
position: relative; position: relative;
width: 100%; width: 100%;
padding-top: 75%; // 4:3 aspect ratio aspect-ratio: 4 / 3;
background: #f5f5f5; border: 1px solid #d3dad9;
border-radius: 13px 13px 0 0;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
overflow: hidden; overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
img { img {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
background: white; background: white;
padding: 12px;
transition: transform 0.3s ease;
}
&:hover img {
transform: scale(1.05);
} }
} }
@@ -89,33 +165,47 @@
border-radius: 20px; border-radius: 20px;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
z-index: 1;
} }
.item-details { .item-details {
padding: 16px; width: 100%;
border: 1px solid #d3dad9;
border-top: none;
border-radius: 0 0 13px 13px;
padding: 12px 16px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
background: #f5f3f9;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 6px;
transition: background 0.3s ease;
} }
.item-name { .item-name {
font-size: 1.1rem; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600;
font-size: clamp(14px, 1.4vw, 18px);
color: #1e3c38;
margin: 0; margin: 0;
color: #333; line-height: 1.3;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
min-height: calc(2 * 1.3em);
} }
.item-rating { .item-rating {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 6px;
font-size: 0.9rem; font-size: 0.85rem;
color: #333; color: #697777;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
.rating-stars { .rating-stars {
color: #ffa502; color: #ffa502;
@@ -133,15 +223,17 @@
.original-price { .original-price {
text-decoration: line-through; text-decoration: line-through;
color: #555; color: #697777;
font-size: 0.9rem; font-size: 0.85rem;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
} }
.discounted-price, .discounted-price,
.current-price { .current-price {
font-size: 1.3rem; font-size: clamp(16px, 1.6vw, 22px);
font-weight: 700; font-weight: 700;
color: var(--primary-color); color: #497671;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
} }
.item-stock { .item-stock {
@@ -153,16 +245,16 @@
.bar-segment { .bar-segment {
width: 20px; width: 20px;
height: 6px; height: 6px;
background: #e0e0e0; background: #d3dad9;
border-radius: 3px; border-radius: 3px;
transition: background 0.2s; transition: background 0.2s;
&.filled.high { &.filled.high {
background: #2ed573; background: #497671;
} }
&.filled.medium { &.filled.medium {
background: #ffa502; background: #a1b4b5;
} }
&.filled.low { &.filled.low {
@@ -175,16 +267,19 @@
.add-to-cart-btn { .add-to-cart-btn {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
background: var(--primary-color); background: #497671;
color: white; color: white;
border: none; border: none;
font-size: 1rem; border-radius: 0 0 13px 13px;
font-size: 0.95rem;
font-weight: 600; font-weight: 600;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: all 0.2s ease;
margin-top: -1px;
&:hover { &:hover {
background: var(--primary-hover); background: #3a5f5b;
} }
&:active { &:active {
@@ -200,8 +295,8 @@
.spinner { .spinner {
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 4px solid #f3f3f3; border: 4px solid #d3dad9;
border-top: 4px solid var(--primary-color); border-top: 4px solid #497671;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto 12px; margin: 0 auto 12px;
@@ -213,22 +308,65 @@
} }
.no-more { .no-more {
color: #555; color: #697777;
padding: 40px 20px; padding: 40px 20px;
} }
// Responsive
@media (max-width: 1200px) {
.items-grid {
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
}
@media (max-width: 992px) {
.items-grid {
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.items-grid { .items-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); grid-template-columns: repeat(2, 1fr);
gap: 16px; gap: 16px;
} }
.item-name { .item-name {
font-size: 0.95rem; font-size: clamp(12px, 3vw, 16px);
} }
.discounted-price, .discounted-price,
.current-price { .current-price {
font-size: 1.1rem; font-size: clamp(14px, 3.5vw, 18px);
}
.item-details {
padding: 10px 12px;
}
}
@media (max-width: 480px) {
.category-container {
padding: 12px;
}
.items-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.item-details {
padding: 8px 10px;
}
.item-card:hover {
transform: translateY(-2px);
}
.add-to-cart-btn {
padding: 10px;
font-size: 0.85rem;
} }
} }

View File

@@ -1,14 +1,16 @@
import { Component, OnInit, OnDestroy, signal, HostListener, ChangeDetectionStrategy } from '@angular/core'; import { Component, OnInit, OnDestroy, signal, HostListener, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { DecimalPipe } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService, CartService } from '../../services'; import { ApiService, CartService } from '../../services';
import { Item } from '../../models'; import { Item } from '../../models';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
@Component({ @Component({
selector: 'app-category', selector: 'app-category',
standalone: true, imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe],
imports: [CommonModule, RouterLink],
templateUrl: './category.component.html', templateUrl: './category.component.html',
styleUrls: ['./category.component.scss'], styleUrls: ['./category.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@@ -24,6 +26,7 @@ export class CategoryComponent implements OnInit, OnDestroy {
private readonly count = 20; private readonly count = 20;
private isLoadingMore = false; private isLoadingMore = false;
private routeSubscription?: Subscription; private routeSubscription?: Subscription;
private scrollTimeout?: ReturnType<typeof setTimeout>;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -41,6 +44,7 @@ export class CategoryComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.routeSubscription?.unsubscribe(); this.routeSubscription?.unsubscribe();
if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
} }
resetAndLoad(): void { resetAndLoad(): void {
@@ -80,8 +84,6 @@ export class CategoryComponent implements OnInit, OnDestroy {
}); });
} }
private scrollTimeout: any;
@HostListener('window:scroll') @HostListener('window:scroll')
onScroll(): void { onScroll(): void {
if (this.scrollTimeout) clearTimeout(this.scrollTimeout); if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
@@ -102,16 +104,7 @@ export class CategoryComponent implements OnInit, OnDestroy {
this.cartService.addItem(itemID); this.cartService.addItem(itemID);
} }
getDiscountedPrice(item: Item): number { readonly getDiscountedPrice = getDiscountedPrice;
return item.price * (1 - item.discount / 100); readonly getMainImage = getMainImage;
} readonly trackByItemId = trackByItemId;
getMainImage(item: Item): string {
return item.photos?.[0]?.url || '';
}
// TrackBy function for performance optimization
trackByItemId(index: number, item: Item): number {
return item.itemID;
}
} }

View File

@@ -2,14 +2,14 @@
@if (loading()) { @if (loading()) {
<div class="loading"> <div class="loading">
<div class="spinner"></div> <div class="spinner"></div>
<p>Загрузка подкатегорий...</p> <p>{{ 'subcategories.loading' | translate }}</p>
</div> </div>
} }
@if (error()) { @if (error()) {
<div class="error"> <div class="error">
<p>{{ error() }}</p> <p>{{ error() }}</p>
<button (click)="ngOnInit()">Попробовать снова</button> <button (click)="ngOnInit()">{{ 'subcategories.retry' | translate }}</button>
</div> </div>
} }
@@ -18,21 +18,37 @@
<h2>{{ parentName() }}</h2> <h2>{{ parentName() }}</h2>
</header> </header>
@if (subcategories().length > 0) {
<div class="categories-grid"> <div class="categories-grid">
@for (cat of subcategories(); track trackByCategoryId($index, cat)) { @for (cat of subcategories(); track trackByCategoryId($index, cat)) {
<div class="category-card"> <a [routerLink]="['/category', cat.categoryID] | langRoute" class="category-card">
<a [routerLink]="['/category', cat.categoryID]" class="category-link"> <div class="category-image">
<div class="category-media">
@if (cat.icon) { @if (cat.icon) {
<img [src]="cat.icon" [alt]="cat.name" loading="lazy" decoding="async" /> <img [src]="cat.icon" [alt]="cat.name" loading="lazy" decoding="async" />
} @else { } @else {
<div class="category-fallback">{{ cat.name }}</div> <div class="category-fallback">{{ cat.name.charAt(0) }}</div>
} }
</div> </div>
<h3>{{ cat.name }}</h3> <div class="category-info">
<h3 class="category-name">{{ cat.name }}</h3>
</div>
</a> </a>
</div>
} }
</div> </div>
} @else {
<div class="no-subcats">
<div class="no-subcats-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="7" height="7" rx="1.5" stroke="#a1b4b5" stroke-width="1.5"/>
<rect x="14" y="3" width="7" height="7" rx="1.5" stroke="#d3dad9" stroke-width="1.5"/>
<rect x="3" y="14" width="7" height="7" rx="1.5" stroke="#d3dad9" stroke-width="1.5"/>
<rect x="14" y="14" width="7" height="7" rx="1.5" stroke="#d3dad9" stroke-width="1.5"/>
</svg>
</div>
<h3>{{ 'subcategories.emptyTitle' | translate }}</h3>
<p>{{ 'subcategories.emptyDesc' | translate }}</p>
<a [routerLink]="'/' | langRoute" class="no-subcats-btn">{{ 'subcategories.goHome' | translate }}</a>
</div>
}
} }
</div> </div>

View File

@@ -1,9 +1,9 @@
.subcategories-container { .subcategories-container {
max-width: 1100px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 24px; padding: 24px;
// Loading состояние // Loading state
.loading { .loading {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -15,20 +15,21 @@
.spinner { .spinner {
width: 48px; width: 48px;
height: 48px; height: 48px;
border: 4px solid #f3f4f6; border: 4px solid #d3dad9;
border-top-color: #3b82f6; border-top-color: #497671;
border-radius: 50%; border-radius: 50%;
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite;
} }
p { p {
color: #6b7280; color: #697777;
font-size: 1rem; font-size: 1rem;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0; margin: 0;
} }
} }
// Error состояние // Error state
.error { .error {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -41,25 +42,27 @@
p { p {
color: #dc2626; color: #dc2626;
font-size: 1rem; font-size: 1rem;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
text-align: center; text-align: center;
margin: 0; margin: 0;
} }
button { button {
padding: 10px 24px; padding: 10px 24px;
background: #3b82f6; background: #497671;
color: white; color: white;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 500; font-weight: 500;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
background: #2563eb; background: #3a5f5b;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); box-shadow: 0 4px 12px rgba(73, 118, 113, 0.3);
} }
&:active { &:active {
@@ -68,145 +71,192 @@
} }
} }
.sub-header { // Empty subcategories state
margin-bottom: 24px; .no-subcats {
position: relative; text-align: center;
padding-bottom: 12px; padding: 80px 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
&::after { .no-subcats-icon {
content: ''; width: 100px;
position: absolute; height: 100px;
bottom: 0; display: flex;
left: 0; align-items: center;
width: 60px; justify-content: center;
height: 3px; background: #f5f3f9;
background: linear-gradient(90deg, #3b82f6, #8b5cf6); border-radius: 50%;
border-radius: 2px; margin-bottom: 24px;
} }
h2 { h3 {
font-size: 1.75rem; font-size: 1.25rem;
color: #1f2937; font-weight: 700;
margin: 0; color: #1e3c38;
margin: 0 0 8px 0;
}
p {
font-size: 0.95rem;
color: #697777;
margin: 0 0 24px 0;
max-width: 340px;
line-height: 1.5;
}
.no-subcats-btn {
display: inline-flex;
align-items: center;
padding: 10px 28px;
background: #497671;
color: white;
border-radius: 13px;
font-family: "DM Sans", sans-serif;
font-size: 0.9375rem;
font-weight: 600; font-weight: 600;
text-decoration: none;
transition: background 0.2s ease, transform 0.15s ease;
&:hover {
background: #3d635f;
transform: translateY(-1px);
}
}
}
.sub-header {
margin-bottom: 28px;
h2 {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 2rem;
color: #1e3c38;
margin: 0;
font-weight: 700;
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
} }
.categories-grid { .categories-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-template-columns: repeat(4, 1fr);
gap: 20px; gap: 30px;
width: 100%;
} }
.category-card { .category-card {
background: white; width: 100%;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; text-decoration: none;
transition: all 0.3s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
animation: fadeInUp 0.5s ease backwards;
cursor: pointer;
// Анимация появления с задержкой для каждой карточки
@for $i from 1 through 20 {
&:nth-child(#{$i}) {
animation-delay: #{$i * 0.05}s;
}
}
&:hover { &:hover {
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0,0,0,0.12);
.category-image {
box-shadow: 0 6px 8px 0 rgba(0, 0, 0, 0.2);
} }
.category-link { .category-info {
display: flex; box-shadow: 0 6px 8px 0 rgba(0, 0, 0, 0.2);
flex-direction: column; }
flex: 1; }
text-decoration: none; }
color: inherit;
position: relative;
min-height: 200px;
.category-media { .category-image {
position: absolute; width: 100%;
top: 0; aspect-ratio: 4 / 3;
left: 0; border: 1px solid #d3dad9;
right: 0; border-radius: 13px 13px 0 0;
bottom: 0; box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; background: #f5f5f5;
background: linear-gradient(135deg, #f6f7fb 0%, #e9ecf5 100%);
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
&:hover .category-media { &:hover img {
transform: scale(1.05); transform: scale(1.05);
} }
.category-media img {
width: 100%;
height: 100%;
object-fit: contain;
background: white;
transition: opacity 0.3s ease;
} }
.category-fallback { .category-fallback {
text-align: center; width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 5rem;
font-weight: 700;
color: #497671;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%);
}
.category-info {
width: 100%;
border: 1px solid #d3dad9;
border-top: none;
border-radius: 0 0 13px 13px;
padding: 12px 16px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
background: #f5f3f9;
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
transition: background 0.3s ease;
}
.category-name {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600; font-weight: 600;
font-size: 1.1rem; font-size: clamp(14px, 1.4vw, 18px);
padding: 20px; color: #1e3c38;
color: #6b7280;
}
h3 {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
margin: 0; margin: 0;
font-size: 0.95rem; line-height: 1.3;
color: white; display: -webkit-box;
padding: 12px 14px; line-clamp: 2;
background: linear-gradient(to top, rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.4) 70%, transparent); -webkit-line-clamp: 2;
z-index: 1; -webkit-box-orient: vertical;
font-weight: 500; overflow: hidden;
transition: all 0.3s ease; text-overflow: ellipsis;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); min-height: calc(2 * 1.3em);
} }
&:hover h3 { // Keyframes
padding: 16px 14px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.5) 70%, transparent);
}
}
}
// Keyframes для анимаций
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
@keyframes fadeInUp { // Responsive
from { @media (max-width: 1200px) {
opacity: 0; padding: 24px;
transform: translateY(20px);
} .categories-grid {
to { grid-template-columns: repeat(3, 1fr);
opacity: 1; gap: 24px;
transform: translateY(0); }
}
@media (max-width: 992px) {
.categories-grid {
grid-template-columns: repeat(3, 1fr);
gap: 20px;
} }
} }
// Мобильная версия
@media (max-width: 768px) { @media (max-width: 768px) {
padding: 16px; padding: 16px;
@@ -220,24 +270,14 @@
.categories-grid { .categories-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 12px; gap: 16px;
} }
.category-card { .category-info {
border-radius: 10px; padding: 10px 12px;
.category-link {
min-height: 140px;
h3 {
font-size: 0.85rem;
padding: 10px 8px;
}
}
} }
} }
// Очень маленькие экраны
@media (max-width: 480px) { @media (max-width: 480px) {
padding: 12px; padding: 12px;
@@ -250,52 +290,16 @@
} }
.categories-grid { .categories-grid {
gap: 10px; grid-template-columns: repeat(2, 1fr);
gap: 12px;
} }
.category-card { .category-info {
.category-link { padding: 8px 10px;
min-height: 120px;
h3 {
font-size: 0.8rem;
padding: 8px 6px;
} }
.category-fallback { .category-card:hover {
font-size: 0.95rem; transform: translateY(-2px);
padding: 12px;
}
}
}
}
// Большие экраны
@media (min-width: 1200px) {
padding: 32px;
.sub-header {
margin-bottom: 28px;
h2 {
font-size: 2rem;
}
}
.categories-grid {
gap: 24px;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.category-card {
.category-link {
min-height: 220px;
h3 {
font-size: 1rem;
padding: 14px 16px;
}
}
} }
} }
} }

View File

@@ -1,37 +1,48 @@
import { Component, OnInit, signal, ChangeDetectionStrategy } from '@angular/core'; import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ApiService } from '../../services'; import { ApiService, LanguageService } from '../../services';
import { Category } from '../../models'; import { Category } from '../../models';
import { Subscription } from 'rxjs';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service';
@Component({ @Component({
selector: 'app-subcategories', selector: 'app-subcategories',
standalone: true, imports: [RouterLink, LangRoutePipe, TranslatePipe],
imports: [CommonModule, RouterLink],
templateUrl: './subcategories.component.html', templateUrl: './subcategories.component.html',
styleUrls: ['./subcategories.component.scss'], styleUrls: ['./subcategories.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SubcategoriesComponent implements OnInit { export class SubcategoriesComponent implements OnInit, OnDestroy {
categories = signal<Category[]>([]); categories = signal<Category[]>([]);
subcategories = signal<Category[]>([]); subcategories = signal<Category[]>([]);
loading = signal(true); loading = signal(true);
error = signal<string | null>(null); error = signal<string | null>(null);
private i18n = inject(TranslateService);
parentName = signal<string>(''); parentName = signal<string>('');
private routeSubscription?: Subscription;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private apiService: ApiService private apiService: ApiService,
private langService: LanguageService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.route.params.subscribe(params => { this.routeSubscription = this.route.params.subscribe(params => {
const id = parseInt(params['id'], 10); const id = parseInt(params['id'], 10);
this.loadForParent(id); this.loadForParent(id);
}); });
} }
ngOnDestroy(): void {
this.routeSubscription?.unsubscribe();
}
private loadForParent(parentID: number): void { private loadForParent(parentID: number): void {
this.loading.set(true); this.loading.set(true);
this.apiService.getCategories().subscribe({ this.apiService.getCategories().subscribe({
@@ -39,11 +50,12 @@ export class SubcategoriesComponent implements OnInit {
this.categories.set(cats); this.categories.set(cats);
const subs = cats.filter(c => c.parentID === parentID); const subs = cats.filter(c => c.parentID === parentID);
const parent = cats.find(c => c.categoryID === parentID); const parent = cats.find(c => c.categoryID === parentID);
this.parentName.set(parent ? parent.name : 'Категория'); this.parentName.set(parent ? parent.name : this.i18n.t('home.categoriesTitle'));
if (!subs || subs.length === 0) { if (!subs || subs.length === 0) {
// No subcategories: redirect to items list for this category // No subcategories: redirect to items list for this category
this.router.navigate(['/category', parentID, 'items'], { replaceUrl: true }); const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}/category`, parentID, 'items'], { replaceUrl: true });
} else { } else {
this.subcategories.set(subs); this.subcategories.set(subs);
} }

View File

@@ -3,10 +3,10 @@
<div class="novo-home"> <div class="novo-home">
<section class="novo-hero novo-hero-compact"> <section class="novo-hero novo-hero-compact">
<div class="novo-hero-content"> <div class="novo-hero-content">
<h1 class="novo-hero-title">Добро пожаловать в {{ brandName }}</h1> <h1 class="novo-hero-title">{{ 'home.welcomeTo' | translate:{ brand: brandName } }}</h1>
<p class="novo-hero-subtitle">Найдите всё, что нужно, в одном месте</p> <p class="novo-hero-subtitle">{{ 'home.subtitle' | translate }}</p>
<a routerLink="/search" class="novo-hero-btn"> <a [routerLink]="'/search' | langRoute" class="novo-hero-btn">
Начать поиск {{ 'home.startSearch' | translate }}
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="5" y1="12" x2="19" y2="12"></line> <line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline> <polyline points="12 5 19 12 12 19"></polyline>
@@ -21,36 +21,36 @@
@if (loading()) { @if (loading()) {
<div class="novo-loading"> <div class="novo-loading">
<div class="novo-spinner"></div> <div class="novo-spinner"></div>
<p>Загружаем категории...</p> <p>{{ 'home.loading' | translate }}</p>
</div> </div>
} }
@if (error()) { @if (error()) {
<div class="novo-error"> <div class="novo-error">
<div class="novo-error-icon">⚠️</div> <div class="novo-error-icon">⚠️</div>
<h3>Что-то пошло не так</h3> <h3>{{ 'home.errorTitle' | translate }}</h3>
<p>{{ error() }}</p> <p>{{ error() }}</p>
<button (click)="loadCategories()" class="novo-retry-btn">Попробовать снова</button> <button (click)="loadCategories()" class="novo-retry-btn">{{ 'home.retry' | translate }}</button>
</div> </div>
} }
@if (!loading() && !error()) { @if (!loading() && !error()) {
<section class="novo-categories"> <section class="novo-categories">
<div class="novo-section-header"> <div class="novo-section-header">
<h2>Категории товаров</h2> <h2>{{ 'home.categoriesTitle' | translate }}</h2>
<p>Выберите интересующую категорию</p> <p>{{ 'home.categoriesSubtitle' | translate }}</p>
</div> </div>
@if (getTopLevelCategories().length === 0) { @if (topLevelCategories().length === 0) {
<div class="novo-empty"> <div class="novo-empty">
<div class="novo-empty-icon">📦</div> <div class="novo-empty-icon">📦</div>
<h3>Категории скоро появятся</h3> <h3>{{ 'home.categoriesEmpty' | translate }}</h3>
<p>Мы работаем над наполнением каталога</p> <p>{{ 'home.categoriesEmptyDesc' | translate }}</p>
</div> </div>
} @else { } @else {
<div class="novo-categories-grid"> <div class="novo-categories-grid">
@for (category of getTopLevelCategories(); track category.categoryID) { @for (category of topLevelCategories(); track category.categoryID) {
<a [routerLink]="['/category', category.categoryID]" class="novo-category-card"> <a [routerLink]="['/category', category.categoryID] | langRoute" class="novo-category-card">
<div class="novo-category-image"> <div class="novo-category-image">
@if (category.icon) { @if (category.icon) {
<img [src]="category.icon" [alt]="category.name" loading="lazy" /> <img [src]="category.icon" [alt]="category.name" loading="lazy" />
@@ -72,54 +72,77 @@
} }
</div> </div>
} @else { } @else {
<!-- DEXAR VERSION - Original --> <!-- DEXAR VERSION - Redesigned 2026 -->
<div class="home-container"> <div class="dexar-home">
<header class="hero hero-compact"> <!-- Hero Section with Full Width Image -->
<h1>{{ brandName }}</h1> <section class="dexar-hero">
<p>Ваш маркетплейс товаров и услуг</p> <div class="dexar-hero-overlay">
</header> <div class="dexar-hero-content">
<h1 class="dexar-hero-title">{{ 'home.dexarHeroTitle' | translate }}</h1>
<p class="dexar-hero-subtitle">{{ 'home.dexarHeroSubtitle' | translate }}</p>
<p class="dexar-hero-tagline">{{ 'home.dexarHeroTagline' | translate }}</p>
<div class="dexar-hero-actions">
<a (click)="scrollToCatalog()" class="dexar-btn-primary">
{{ 'home.goToCatalog' | translate }}
</a>
<button (click)="navigateToSearch()" class="dexar-btn-secondary">
{{ 'home.findProduct' | translate }}
<svg width="11" height="16" viewBox="0 0 11 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L9 8L1 15" stroke="#1E3C38" stroke-width="2"/>
</svg>
</button>
</div>
</div>
</div>
</section>
<!-- Items Carousel --> <!-- Items Carousel -->
<app-items-carousel /> <app-items-carousel />
@if (loading()) { @if (loading()) {
<div class="loading"> <div class="dexar-loading">
<div class="spinner"></div> <div class="dexar-spinner"></div>
<p>Загрузка категорий...</p> <p>{{ 'home.loadingDexar' | translate }}</p>
</div> </div>
} }
@if (error()) { @if (error()) {
<div class="error"> <div class="dexar-error">
<p>{{ error() }}</p> <p>{{ error() }}</p>
<button (click)="loadCategories()">Попробовать снова</button> <button (click)="loadCategories()" class="dexar-retry-btn">{{ 'home.retry' | translate }}</button>
</div> </div>
} }
@if (!loading() && !error()) { @if (!loading() && !error()) {
<section class="categories"> <section class="dexar-categories" id="catalog">
<h2>Категории</h2> <h2 class="dexar-categories-title">{{ 'home.catalogTitle' | translate }}</h2>
@if (getTopLevelCategories().length === 0) { @if (topLevelCategories().length === 0) {
<div class="empty-categories"> <div class="dexar-empty-categories">
<div class="empty-icon">📦</div> <div class="dexar-empty-icon">📦</div>
<h3>Категории пока отсутствуют</h3> <h3>{{ 'home.emptyCategoriesDexar' | translate }}</h3>
<p>Скоро здесь появятся категории товаров</p> <p>{{ 'home.categoriesSoonDexar' | translate }}</p>
</div> </div>
} @else { } @else {
<div class="categories-grid"> <div class="dexar-categories-grid">
@for (category of getTopLevelCategories(); track category.categoryID) { @for (category of topLevelCategories(); track category.categoryID) {
<div class="category-card"> <a [routerLink]="['/category', category.categoryID] | langRoute"
<a [routerLink]="['/category', category.categoryID]" class="category-link"> class="dexar-category-card"
<div class="category-media"> [class.dexar-category-card--wide]="isWideCategory(category.categoryID)">
@if (category.icon) { <div class="dexar-category-image">
@if (isWideCategory(category.categoryID) && category.wideBanner) {
<img [src]="category.wideBanner" [alt]="category.name" loading="lazy" decoding="async" />
} @else if (category.icon) {
<img [src]="category.icon" [alt]="category.name" loading="lazy" decoding="async" /> <img [src]="category.icon" [alt]="category.name" loading="lazy" decoding="async" />
} @else { } @else {
<div class="category-fallback">{{ category.name }}</div> <div class="dexar-category-fallback">{{ category.name.charAt(0) }}</div>
} }
</div> </div>
<h3>{{ category.name }}</h3> <div class="dexar-category-info">
</a> <h3 class="dexar-category-name">{{ category.name }}</h3>
<p class="dexar-category-count">{{ 'home.itemsCount' | translate:{ count: getItemCount(category.categoryID) } }}</p>
</div> </div>
</a>
} }
</div> </div>
} }

View File

@@ -1,321 +1,25 @@
.home-container { // ========== SHARED ANIMATIONS ==========
max-width: 1200px;
margin: 0 auto;
padding: 20px;
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn { @keyframes fadeIn {
from { from { opacity: 0; transform: translateY(10px); }
opacity: 0; to { opacity: 1; transform: translateY(0); }
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.hero { @keyframes fadeInUp {
text-align: center; from { opacity: 0; transform: translateY(30px); }
padding: 80px 20px; to { opacity: 1; transform: translateY(0); }
background: var(--gradient-hero);
color: white;
border-radius: var(--radius-xl);
margin-bottom: 50px;
position: relative;
overflow: hidden;
box-shadow: var(--shadow-lg);
&.hero-compact {
padding: 35px 20px;
margin-bottom: 25px;
h1 {
font-size: 2.2rem;
margin-bottom: 8px;
}
p {
font-size: 1.1rem;
}
}
&::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: pulse 4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 0.5;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
h1 {
font-size: 3.5rem;
margin: 0 0 15px 0;
font-weight: 700;
position: relative;
z-index: 1;
text-shadow: 0 2px 10px rgba(0,0,0,0.2);
animation: slideDown 0.8s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
p {
font-size: 1.4rem;
margin: 0;
opacity: 0.95;
position: relative;
z-index: 1;
animation: slideUp 0.8s ease-out 0.2s both;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 0.95;
transform: translateY(0);
}
}
} }
.loading, @keyframes slideDown {
.error { from { opacity: 0; transform: translateY(-30px); }
text-align: center; to { opacity: 1; transform: translateY(0); }
padding: 60px 20px;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } from { transform: rotate(0deg); }
100% { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
.error { // ========== NOVO HOME PAGE STYLES ==========
button {
margin-top: 20px;
padding: 10px 24px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
&:hover {
background: var(--primary-hover);
}
}
}
.categories {
h2 {
font-size: 2rem;
margin-bottom: 30px;
color: #333;
}
}
.empty-categories {
text-align: center;
padding: 60px 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.empty-icon {
font-size: 4rem;
margin-bottom: 20px;
opacity: 0.5;
}
h3 {
font-size: 1.5rem;
color: #333;
margin: 0 0 10px 0;
}
p {
color: #666;
font-size: 1rem;
margin: 0;
}
}
.categories-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 30px;
animation: fadeIn 0.6s ease-in 0.3s both;
}
.category-card {
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--gradient-primary);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 1;
}
&:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: var(--shadow-lg);
&::before {
opacity: 0.1;
}
.category-media img {
transform: scale(1.1);
}
h3 {
color: var(--primary-color);
}
}
}
.category-link {
display: flex;
flex-direction: column;
flex: 1;
text-decoration: none;
color: inherit;
position: relative;
min-height: 220px;
z-index: 2;
.category-media {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: linear-gradient(135deg, #f6f7fb 0%, #e9ecf5 100%);
}
.category-media img {
width: 100%;
height: 100%;
object-fit: contain;
background: white;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.category-fallback {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary-color);
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
padding: 20px;
}
h3 {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0;
padding: 20px;
font-size: 1.3rem;
font-weight: 600;
color: #333;
background: linear-gradient(to top, rgba(255,255,255,0.98) 0%, rgba(255,255,255,0.95) 70%, transparent 100%);
z-index: 3;
transition: color 0.3s ease;
}
}
@media (max-width: 768px) {
.home-container {
padding: 15px;
}
.hero {
padding: 50px 20px;
border-radius: 15px;
h1 {
font-size: 2.5rem;
}
p {
font-size: 1.1rem;
}
}
.categories-grid {
grid-template-columns: 1fr;
gap: 20px;
}
.category-card {
&:hover {
transform: translateY(-4px) scale(1);
}
}
}
// ========== novo HOME PAGE STYLES ==========
.novo-home { .novo-home {
min-height: calc(100vh - 200px); min-height: calc(100vh - 200px);
animation: fadeIn 0.6s ease; animation: fadeIn 0.6s ease;
@@ -611,26 +315,6 @@
} }
} }
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (max-width: 968px) { @media (max-width: 968px) {
.novo-hero { .novo-hero {
padding: 4rem 2rem; padding: 4rem 2rem;
@@ -693,3 +377,522 @@
transform: translateY(-4px); transform: translateY(-4px);
} }
} }
// ========== DEXAR REDESIGN 2026 STYLES ==========
.dexar-home {
width: 100%;
overflow-x: hidden;
}
// Hero Section
.dexar-hero {
position: relative;
width: 100%;
height: 600px;
background-image: url('/assets/images/hero_background_desktop.webp');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
overflow: hidden;
// Mobile hero image
@media (max-width: 768px) {
background-image: url('/assets/images/hero_background_mobile.webp');
}
}
.dexar-hero-overlay {
position: relative;
width: 100%;
height: 100%;
background: linear-gradient(
to right,
rgba(255, 255, 255, 0.1) 10%,
rgba(255, 255, 255, 0.1) 10%,
rgba(255, 255, 255, 0.1) 15%);
display: flex;
align-items: center;
padding: 0 56px;
}
.dexar-hero-content {
max-width: 600px;
display: flex;
flex-direction: column;
gap: 12px;
animation: fadeInUp 0.8s ease-out;
}
.dexar-hero-title {
font-weight: 500;
font-size: 42px;
color: var(--text-primary);
line-height: 1.2;
margin: 0;
animation: fadeInUp 0.8s ease-out 0.1s both;
}
.dexar-hero-subtitle {
font-weight: 500;
font-size: 24px;
color: var(--text-primary);
line-height: 1.3;
margin: 0;
animation: fadeInUp 0.8s ease-out 0.2s both;
}
.dexar-hero-tagline {
font-weight: 500;
font-size: 24px;
color: var(--text-primary);
line-height: 1.3;
margin: 0;
animation: fadeInUp 0.8s ease-out 0.3s both;
}
.dexar-hero-actions {
display: flex;
gap: 16px;
margin-top: 12px;
animation: fadeInUp 0.8s ease-out 0.4s both;
}
.dexar-btn-primary {
display: flex;
align-items: center;
justify-content: center;
width: 280px;
height: 48px;
background: var(--gradient-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
font-weight: 500;
font-size: 20px;
color: #ffffff;
text-decoration: none;
letter-spacing: 1.08px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(73, 118, 113, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(73, 118, 113, 0.4);
}
&:active {
transform: translateY(0px);
box-shadow: 0 2px 8px rgba(73, 118, 113, 0.3);
}
}
.dexar-btn-secondary {
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
width: 220px;
height: 48px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
font-weight: 500;
font-size: 20px;
color: var(--text-primary);
letter-spacing: 1.08px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
svg {
width: 11px;
height: 16px;
transition: transform 0.3s ease;
}
&:hover {
background: #ffffff;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
svg {
transform: translateX(3px);
}
}
&:active {
transform: translateY(0px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
// Loading & Error States
.dexar-loading,
.dexar-error {
text-align: center;
padding: 60px 20px;
max-width: 1200px;
margin: 0 auto;
}
.dexar-spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
.dexar-error {
button {
margin-top: 20px;
padding: 12px 28px;
background: var(--primary-color);
color: white;
border: none;
border-radius: var(--radius-lg);
cursor: pointer;
font-size: 1.1rem;
font-weight: 500;
transition: all 0.3s ease;
&:hover {
background: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(73, 118, 113, 0.3);
}
}
}
// Categories Section
.dexar-categories {
max-width: 1440px;
margin: 50px auto;
padding: 0 56px;
}
.dexar-categories-title {
font-size: 2.5rem;
font-weight: 600;
margin-bottom: 40px;
color: var(--text-primary);
}
.dexar-empty-categories {
text-align: center;
padding: 80px 20px;
background: white;
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
.dexar-empty-icon {
font-size: 4rem;
margin-bottom: 20px;
opacity: 0.5;
}
h3 {
font-size: 1.8rem;
color: var(--text-primary);
margin: 0 0 12px 0;
font-weight: 600;
}
p {
color: var(--text-secondary);
font-size: 1.1rem;
margin: 0;
}
}
.dexar-categories-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30px;
animation: fadeIn 0.6s ease-in 0.3s both;
width: 100%;
}
.dexar-category-card {
width: 100%;
display: flex;
flex-direction: column;
text-decoration: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-4px);
.dexar-category-image {
box-shadow: 0 6px 8px 0 rgba(0, 0, 0, 0.2);
}
.dexar-category-info {
box-shadow: 0 6px 8px 0 rgba(0, 0, 0, 0.2);
}
}
}
.dexar-category-image {
width: 100%;
aspect-ratio: 4 / 3;
border: 1px solid var(--border-color);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
position: relative;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
&:hover img {
transform: scale(1.05);
}
}
.dexar-category-fallback {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 5rem;
font-weight: 700;
color: var(--primary-color);
background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
}
.dexar-category-info {
width: 100%;
border: 1px solid var(--border-color);
border-top: none;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
padding: 12px 16px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
background: #f5f3f9;
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
transition: background 0.3s ease;
}
.dexar-category-name {
font-weight: 600;
font-size: clamp(14px, 1.4vw, 18px);
color: var(--text-primary);
margin: 0;
line-height: 1.3;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
min-height: calc(2 * 1.3em);
}
.dexar-category-count {
font-weight: 600;
font-size: clamp(11px, 1vw, 13px);
color: var(--text-secondary);
margin: 0;
line-height: 1.2;
}
// Wide category card (spans 2 columns)
.dexar-category-card--wide {
grid-column: span 2;
.dexar-category-image {
aspect-ratio: 8 / 3;
img {
object-fit: cover;
}
}
}
// Responsive Design
@media (max-width: 1200px) {
.dexar-hero {
height: 380px;
}
.dexar-hero-overlay {
padding: 0 40px;
}
.dexar-hero-title {
font-size: 38px;
}
.dexar-hero-subtitle,
.dexar-hero-tagline {
font-size: 22px;
}
.dexar-categories {
padding: 0 40px;
}
.dexar-categories-grid {
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
}
@media (max-width: 992px) {
.dexar-hero {
height: 340px;
}
.dexar-hero-overlay {
padding: 0 32px;
}
.dexar-hero-title {
font-size: 34px;
}
.dexar-hero-subtitle,
.dexar-hero-tagline {
font-size: 20px;
}
.dexar-btn-primary,
.dexar-btn-secondary {
height: 44px;
font-size: 18px;
}
.dexar-btn-primary {
width: 240px;
}
.dexar-btn-secondary {
width: 200px;
}
.dexar-categories {
padding: 0 32px;
}
.dexar-categories-grid {
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
}
@media (max-width: 768px) {
.dexar-hero {
height: 320px;
background-position: right center;
}
.dexar-hero-overlay {
padding: 0 20px;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.45) 0%,
rgba(255, 255, 255, 0.15) 100%
);
}
.dexar-hero-title {
font-size: 28px;
}
.dexar-hero-subtitle,
.dexar-hero-tagline {
font-size: 17px;
}
.dexar-hero-actions {
flex-direction: row;
gap: 10px;
width: 100%;
}
.dexar-btn-primary,
.dexar-btn-secondary {
flex: 1;
min-width: 0;
max-width: 180px;
height: 40px;
font-size: 14px;
letter-spacing: 0.5px;
}
.dexar-categories {
margin: 40px auto;
padding: 0 20px;
}
.dexar-categories-title {
font-size: 2rem;
margin-bottom: 30px;
}
.dexar-categories-grid {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.dexar-category-info {
padding: 10px 12px;
}
}
@media (max-width: 480px) {
.dexar-hero {
height: 280px;
}
.dexar-hero-title {
font-size: 24px;
}
.dexar-hero-subtitle,
.dexar-hero-tagline {
font-size: 15px;
}
.dexar-hero-actions {
gap: 10px;
}
.dexar-btn-primary,
.dexar-btn-secondary {
height: 38px;
font-size: 13px;
max-width: 160px;
}
.dexar-categories {
padding: 0 16px;
}
.dexar-categories-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.dexar-category-info {
padding: 8px 10px;
}
.dexar-category-card:hover {
transform: translateY(-2px);
}
}

View File

@@ -1,15 +1,15 @@
import { Component, OnInit, signal, computed, ChangeDetectionStrategy } from '@angular/core'; import { Component, OnInit, signal, computed, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { Router, RouterLink } from '@angular/router';
import { RouterLink } from '@angular/router'; import { ApiService, LanguageService } from '../../services';
import { ApiService } from '../../services';
import { Category } from '../../models'; import { Category } from '../../models';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { ItemsCarouselComponent } from '../../components/items-carousel/items-carousel.component'; import { ItemsCarouselComponent } from '../../components/items-carousel/items-carousel.component';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
standalone: true, imports: [RouterLink, ItemsCarouselComponent, LangRoutePipe, TranslatePipe],
imports: [CommonModule, RouterLink, ItemsCarouselComponent],
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'], styleUrls: ['./home.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@@ -18,12 +18,22 @@ export class HomeComponent implements OnInit {
brandName = environment.brandFullName; brandName = environment.brandFullName;
isnovo = environment.theme === 'novo'; isnovo = environment.theme === 'novo';
categories = signal<Category[]>([]); categories = signal<Category[]>([]);
wideCategories = signal<Set<number>>(new Set());
loading = signal(true); loading = signal(true);
error = signal<string | null>(null); error = signal<string | null>(null);
// Memoized computed values for performance // Memoized computed values for performance
topLevelCategories = computed(() => { topLevelCategories = computed(() => {
return this.categories().filter(cat => cat.parentID === 0); return this.categories()
.filter(cat => cat.parentID === 0)
.sort((a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity));
});
// Memoized item count lookup
private itemCountMap = computed(() => {
const map = new Map<number, number>();
this.categories().forEach(cat => map.set(cat.categoryID, cat.itemCount || 0));
return map;
}); });
// Cache subcategories by parent ID // Cache subcategories by parent ID
@@ -40,7 +50,7 @@ export class HomeComponent implements OnInit {
return cache; return cache;
}); });
constructor(private apiService: ApiService) {} constructor(private apiService: ApiService, private router: Router, private langService: LanguageService) {}
ngOnInit(): void { ngOnInit(): void {
this.loadCategories(); this.loadCategories();
@@ -52,6 +62,7 @@ export class HomeComponent implements OnInit {
next: (categories) => { next: (categories) => {
this.categories.set(categories); this.categories.set(categories);
this.loading.set(false); this.loading.set(false);
this.detectWideImages(categories);
}, },
error: (err) => { error: (err) => {
this.error.set('Failed to load categories'); this.error.set('Failed to load categories');
@@ -61,11 +72,64 @@ export class HomeComponent implements OnInit {
}); });
} }
getTopLevelCategories(): Category[] { getItemCount(categoryID: number): number {
return this.topLevelCategories(); return this.itemCountMap().get(categoryID) || 0;
} }
getSubCategories(parentID: number): Category[] { getSubCategories(parentID: number): Category[] {
return this.subcategoriesCache().get(parentID) || []; return this.subcategoriesCache().get(parentID) || [];
} }
isWideCategory(categoryID: number): boolean {
return this.wideCategories().has(categoryID);
}
private detectWideImages(categories: Category[]): void {
const topLevel = categories.filter(c => c.parentID === 0);
topLevel.forEach(cat => {
if (!cat.wideBanner) return;
const img = new Image();
img.onload = () => {
const ratio = img.naturalWidth / img.naturalHeight;
if (ratio > 2) {
this.wideCategories.update(set => {
const next = new Set(set);
next.add(cat.categoryID);
return next;
});
}
};
img.src = cat.wideBanner;
});
}
navigateToSearch(): void {
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}/search`]);
}
scrollToCatalog(): void {
const target = document.getElementById('catalog');
if (!target) return;
const targetY = target.getBoundingClientRect().top + window.scrollY;
const startY = window.scrollY;
const distance = targetY - startY;
const duration = 1200;
let start: number | null = null;
const easeInOutCubic = (t: number) =>
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
const step = (timestamp: number) => {
if (!start) start = timestamp;
const elapsed = timestamp - start;
const progress = Math.min(elapsed / duration, 1);
window.scrollTo(0, startY + distance * easeInOutCubic(progress));
if (progress < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
} }

View File

@@ -1,13 +1,12 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
@Component({ @Component({
selector: 'app-about', selector: 'app-about',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './about.component.html', templateUrl: './about.component.html',
styleUrls: ['./about.component.scss'] styleUrls: ['./about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AboutComponent { export class AboutComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -1,13 +1,12 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
@Component({ @Component({
selector: 'app-contacts', selector: 'app-contacts',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './contacts.component.html', templateUrl: './contacts.component.html',
styleUrls: ['./contacts.component.scss'] styleUrls: ['./contacts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ContactsComponent { export class ContactsComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -1,13 +1,12 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
@Component({ @Component({
selector: 'app-delivery', selector: 'app-delivery',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './delivery.component.html', templateUrl: './delivery.component.html',
styleUrls: ['./delivery.component.scss'] styleUrls: ['./delivery.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DeliveryComponent { export class DeliveryComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -232,7 +232,7 @@
<div class="faq-item"> <div class="faq-item">
<h3>Какие товары вернуть нельзя?</h3> <h3>Какие товары вернуть нельзя?</h3>
<p>Нельзя вернуть товары, перечисленные в Постановлении Правительства РФ №2463: медикаменты, косметику, бельё, активированные цифровые продукты и индивидуальные заказы. Подробности смотрите в разделе <a routerLink="/return-policy">«Политика возврата»</a>.</p> <p>Нельзя вернуть товары, перечисленные в Постановлении Правительства РФ №2463: медикаменты, косметику, бельё, активированные цифровые продукты и индивидуальные заказы. Подробности смотрите в разделе <a [routerLink]="'/return-policy' | langRoute">«Политика возврата»</a>.</p>
</div> </div>
<div class="faq-item"> <div class="faq-item">
@@ -274,7 +274,7 @@
<div class="faq-item"> <div class="faq-item">
<h3>Как вы защищаете мои личные данные?</h3> <h3>Как вы защищаете мои личные данные?</h3>
<p>Используем современные методы шифрования SSL/TLS, не храним реквизиты карт, выполняем требования закона №152-ФЗ о защите персональной информации. Подробности — в <a routerLink="/privacy-policy">Политике конфиденциальности</a>.</p> <p>Используем современные методы шифрования SSL/TLS, не храним реквизиты карт, выполняем требования закона №152-ФЗ о защите персональной информации. Подробности — в <a [routerLink]="'/privacy-policy' | langRoute">Политике конфиденциальности</a>.</p>
</div> </div>
<div class="faq-item"> <div class="faq-item">

View File

@@ -1,13 +1,14 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { LangRoutePipe } from '../../../pipes/lang-route.pipe';
@Component({ @Component({
selector: 'app-faq', selector: 'app-faq',
standalone: true, imports: [RouterLink, LangRoutePipe],
imports: [CommonModule],
templateUrl: './faq.component.html', templateUrl: './faq.component.html',
styleUrls: ['./faq.component.scss'] styleUrls: ['./faq.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class FaqComponent { export class FaqComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -1,13 +1,12 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
@Component({ @Component({
selector: 'app-guarantee', selector: 'app-guarantee',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './guarantee.component.html', templateUrl: './guarantee.component.html',
styleUrls: ['./guarantee.component.scss'] styleUrls: ['./guarantee.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class GuaranteeComponent { export class GuaranteeComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -4,14 +4,14 @@
@if (loading()) { @if (loading()) {
<div class="novo-loading"> <div class="novo-loading">
<div class="novo-spinner"></div> <div class="novo-spinner"></div>
<p>Загрузка...</p> <p>{{ 'itemDetail.loading' | translate }}</p>
</div> </div>
} }
@if (error()) { @if (error()) {
<div class="novo-error"> <div class="novo-error">
<p>{{ error() }}</p> <p>{{ error() }}</p>
<a routerLink="/" class="novo-back-link">Вернуться</a> <a [routerLink]="'/' | langRoute" class="novo-back-link">{{ 'itemDetail.back' | translate }}</a>
</div> </div>
} }
@@ -49,7 +49,7 @@
<circle cx="8.5" cy="8.5" r="1.5"></circle> <circle cx="8.5" cy="8.5" r="1.5"></circle>
<path d="M21 15l-5-5L5 21"></path> <path d="M21 15l-5-5L5 21"></path>
</svg> </svg>
<p>Нет изображения</p> <p>{{ 'itemDetail.noImage' | translate }}</p>
</div> </div>
} }
</div> </div>
@@ -76,10 +76,10 @@
</div> </div>
<div class="novo-stock"> <div class="novo-stock">
<span class="stock-label">Наличие:</span> <span class="stock-label">{{ 'itemDetail.stock' | translate }}</span>
<div class="stock-indicator" [class.high]="item()!.remainings === 'high'" [class.medium]="item()!.remainings === 'medium'" [class.low]="item()!.remainings === 'low'"> <div class="stock-indicator" [class.high]="item()!.remainings === 'high'" [class.medium]="item()!.remainings === 'medium'" [class.low]="item()!.remainings === 'low'">
<span class="dot"></span> <span class="dot"></span>
{{ item()!.remainings === 'high' ? 'В наличии' : item()!.remainings === 'medium' ? 'Мало' : 'Осталось немного' }} {{ item()!.remainings === 'high' ? ('itemDetail.inStock' | translate) : item()!.remainings === 'medium' ? ('itemDetail.mediumStock' | translate) : ('itemDetail.lowStock' | translate) }}
</div> </div>
</div> </div>
@@ -89,24 +89,24 @@
<circle cx="20" cy="21" r="1"></circle> <circle cx="20" cy="21" r="1"></circle>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path> <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
</svg> </svg>
Добавить в корзину {{ 'itemDetail.addToCart' | translate }}
</button> </button>
<div class="novo-description"> <div class="novo-description">
<h3>Описание</h3> <h3>{{ 'itemDetail.description' | translate }}</h3>
<div [innerHTML]="getSafeHtml(item()!.description)"></div> <div [innerHTML]="getSafeHtml(item()!.description)"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="novo-reviews"> <div class="novo-reviews">
<h2>Отзывы ({{ item()!.callbacks?.length || 0 }})</h2> <h2>{{ 'itemDetail.reviews' | translate }} ({{ item()!.callbacks?.length || 0 }})</h2>
<!-- novo Review Form --> <!-- novo Review Form -->
<div class="novo-review-form"> <div class="novo-review-form">
<h3>Ваш отзыв</h3> <h3>{{ 'itemDetail.yourReview' | translate }}</h3>
<div class="novo-rating-input"> <div class="novo-rating-input">
<label>Оценка:</label> <label>{{ 'itemDetail.rating' | translate }}</label>
<div class="novo-star-selector"> <div class="novo-star-selector">
@for (star of [1, 2, 3, 4, 5]; track star) { @for (star of [1, 2, 3, 4, 5]; track star) {
<span <span
@@ -120,14 +120,14 @@
</div> </div>
<textarea <textarea
[(ngModel)]="newReview.comment" [(ngModel)]="newReview.comment"
placeholder="Поделитесь своими впечатлениями о товаре..." [placeholder]="'itemDetail.reviewPlaceholder' | translate"
rows="4" rows="4"
class="novo-textarea"> class="novo-textarea">
</textarea> </textarea>
<div class="novo-form-actions"> <div class="novo-form-actions">
<label class="novo-anonymous-toggle"> <label class="novo-anonymous-toggle">
<input type="checkbox" [(ngModel)]="newReview.anonymous"> <input type="checkbox" [(ngModel)]="newReview.anonymous">
<span>Анонимно</span> <span>{{ 'itemDetail.anonymous' | translate }}</span>
</label> </label>
@if (!newReview.anonymous && getUserDisplayName()) { @if (!newReview.anonymous && getUserDisplayName()) {
<span class="novo-username-preview">{{ getUserDisplayName() }}</span> <span class="novo-username-preview">{{ getUserDisplayName() }}</span>
@@ -139,12 +139,12 @@
[class.submitting]="reviewSubmitStatus() === 'loading'"> [class.submitting]="reviewSubmitStatus() === 'loading'">
@if (reviewSubmitStatus() === 'loading') { @if (reviewSubmitStatus() === 'loading') {
<span class="novo-spinner-small"></span> <span class="novo-spinner-small"></span>
Отправка... {{ 'itemDetail.submitting' | translate }}
} @else { } @else {
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/> <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
</svg> </svg>
Отправить {{ 'itemDetail.submit' | translate }}
} }
</button> </button>
</div> </div>
@@ -154,7 +154,7 @@
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M20 6L9 17l-5-5"/> <path d="M20 6L9 17l-5-5"/>
</svg> </svg>
Спасибо за ваш отзыв! {{ 'itemDetail.reviewSuccess' | translate }}
</div> </div>
} }
@@ -163,7 +163,7 @@
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M18 6L6 18M6 6l12 12"/> <path d="M18 6L6 18M6 6l12 12"/>
</svg> </svg>
Ошибка отправки. Попробуйте позже. {{ 'itemDetail.reviewError' | translate }}
</div> </div>
} }
</div> </div>
@@ -174,7 +174,7 @@
<div class="novo-review-card"> <div class="novo-review-card">
<div class="review-header"> <div class="review-header">
<div class="reviewer-info"> <div class="reviewer-info">
<span class="reviewer-name">{{ review.userID || 'Пользователь' }}</span> <span class="reviewer-name">{{ review.userID ? review.userID : ('itemDetail.defaultUser' | translate) }}</span>
@if (review.timestamp) { @if (review.timestamp) {
<span class="review-date">{{ formatDate(review.timestamp) }}</span> <span class="review-date">{{ formatDate(review.timestamp) }}</span>
} }
@@ -185,208 +185,247 @@
</div> </div>
} }
} @else { } @else {
<p class="novo-no-reviews">Пока нет отзывов. Станьте первым!</p> <p class="novo-no-reviews">{{ 'itemDetail.noReviews' | translate }}</p>
} }
</div> </div>
</div> </div>
} }
</div> </div>
} @else { } @else {
<!-- DEXAR VERSION - Original --> <!-- DEXAR VERSION - Redesigned 2026 -->
<div class="item-detail-container"> <div class="dx-item-container">
@if (loading()) { @if (loading()) {
<div class="loading"> <div class="dx-loading">
<div class="spinner"></div> <div class="dx-spinner"></div>
<p>Загрузка товара...</p> <p>{{ 'itemDetail.loadingDexar' | translate }}</p>
</div> </div>
} }
@if (error()) { @if (error()) {
<div class="error"> <div class="dx-error">
<p>{{ error() }}</p> <p>{{ error() }}</p>
<a routerLink="/" class="back-link">Вернуться на главную</a> <a [routerLink]="'/' | langRoute" class="dx-back-link">{{ 'itemDetail.backHome' | translate }}</a>
</div> </div>
} }
@if (item() && !loading()) { @if (item() && !loading()) {
<div class="item-content"> <div class="dx-item-content">
<div class="item-gallery"> <!-- Gallery: thumbnails left + main photo -->
<div class="dx-gallery">
@if (item()?.photos && item()!.photos!.length > 0) { @if (item()?.photos && item()!.photos!.length > 0) {
<div class="main-photo"> <div class="dx-thumbnails">
@if (item()!.photos![selectedPhotoIndex()]?.video) {
<video [src]="item()!.photos![selectedPhotoIndex()].url" controls></video>
} @else {
<img [src]="item()!.photos![selectedPhotoIndex()].url" [alt]="item()!.name" fetchpriority="high" decoding="async" width="600" height="600" />
}
</div>
<div class="photo-thumbnails">
@for (photo of item()!.photos!; track $index) { @for (photo of item()!.photos!; track $index) {
<div <div
class="thumbnail" class="dx-thumb"
[class.active]="selectedPhotoIndex() === $index" [class.active]="selectedPhotoIndex() === $index"
(click)="selectPhoto($index)"> (click)="selectPhoto($index)">
@if (photo.video) { @if (photo.video) {
<div class="video-indicator"></div> <div class="dx-video-badge"></div>
} }
<img [src]="photo.url" [alt]="'Photo ' + ($index + 1)" loading="lazy" decoding="async" /> <img [src]="photo.url" [alt]="('itemDetail.photo' | translate) + ' ' + ($index + 1)" loading="lazy" decoding="async" />
</div> </div>
} }
</div> </div>
}
<div class="dx-main-photo">
@if (item()?.photos && item()!.photos!.length > 0) {
@if (item()!.photos![selectedPhotoIndex()]?.video) {
<video [src]="item()!.photos![selectedPhotoIndex()].url" controls></video>
} @else { } @else {
<div class="main-photo"> <img [src]="item()!.photos![selectedPhotoIndex()].url" [alt]="item()!.name" fetchpriority="high" decoding="async" />
<div class="no-image">📦 Нет изображения</div> }
} @else {
<div class="dx-no-image">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#a1b4b5" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<path d="M21 15l-5-5L5 21"></path>
</svg>
<span>{{ 'itemDetail.noImage' | translate }}</span>
</div> </div>
} }
</div> </div>
<div class="item-info">
<h1>{{ item()!.name }}</h1>
<div class="rating-section">
<span class="rating">{{ getRatingStars(item()!.rating) }} {{ item()!.rating }}</span>
<span class="reviews-count">({{ item()!.callbacks?.length || 0 }} отзывов)</span>
</div> </div>
<div class="price-section"> <!-- Item Info -->
<div class="dx-info">
<h1 class="dx-title">{{ item()!.name }}</h1>
<div class="dx-rating">
<div class="dx-stars">
@for (star of [1, 2, 3, 4, 5]; track star) {
<svg width="18" height="18" viewBox="0 0 24 24" [attr.fill]="star <= item()!.rating ? '#497671' : 'none'" [attr.stroke]="star <= item()!.rating ? '#497671' : '#a1b4b5'" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
}
</div>
<span class="dx-rating-value">{{ item()!.rating }}</span>
<span class="dx-rating-count">({{ item()!.callbacks?.length || 0 }} {{ 'itemDetail.reviewsCount' | translate }})</span>
</div>
<div class="dx-price-block">
@if (item()!.discount > 0) { @if (item()!.discount > 0) {
<div class="discount-info"> <div class="dx-price-row">
<span class="original-price">{{ item()!.price }} {{ item()!.currency }}</span> <span class="dx-old-price">{{ item()!.price }} {{ item()!.currency }}</span>
<span class="discount-badge">-{{ item()!.discount }}%</span> <span class="dx-discount-tag">-{{ item()!.discount }}%</span>
</div> </div>
<div class="current-price">{{ getDiscountedPrice() | number:'1.2-2' }} {{ item()!.currency }}</div>
} @else {
<div class="current-price">{{ item()!.price }} {{ item()!.currency }}</div>
} }
</div> <div class="dx-current-price">
{{ item()!.discount > 0 ? (getDiscountedPrice() | number:'1.2-2') : item()!.price }} {{ item()!.currency }}
<div class="stock-info">
<div class="stock-bar">
<span class="bar-segment" [class.filled]="item()!.remainings === 'high' || item()!.remainings === 'medium' || item()!.remainings === 'low'" [class.high]="item()!.remainings === 'high'" [class.medium]="item()!.remainings === 'medium'" [class.low]="item()!.remainings === 'low'"></span>
<span class="bar-segment" [class.filled]="item()!.remainings === 'high' || item()!.remainings === 'medium'" [class.high]="item()!.remainings === 'high'" [class.medium]="item()!.remainings === 'medium'"></span>
<span class="bar-segment" [class.filled]="item()!.remainings === 'high'" [class.high]="item()!.remainings === 'high'"></span>
</div> </div>
</div> </div>
<button class="add-to-cart-btn" (click)="addToCart()"> <div class="dx-stock">
Добавить в корзину <span class="dx-stock-label">{{ 'itemDetail.stock' | translate }}</span>
<span class="dx-stock-status"
[class.high]="item()!.remainings === 'high'"
[class.medium]="item()!.remainings === 'medium'"
[class.low]="item()!.remainings === 'low'">
<span class="dx-stock-dot"></span>
{{ item()!.remainings === 'high' ? ('itemDetail.inStock' | translate) : item()!.remainings === 'medium' ? ('itemDetail.mediumStock' | translate) : ('itemDetail.lastItems' | translate) }}
</span>
</div>
<button class="dx-add-cart" (click)="addToCart()">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="21" r="1"></circle>
<circle cx="20" cy="21" r="1"></circle>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
</svg>
{{ 'itemDetail.addToCart' | translate }}
</button> </button>
<div class="description-section"> <div class="dx-description">
<h2>Описание</h2> <h2>{{ 'itemDetail.description' | translate }}</h2>
<div class="description" [innerHTML]="getSafeHtml(item()!.description)"></div> <div class="dx-description-text" [innerHTML]="getSafeHtml(item()!.description)"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="reviews-section"> <!-- Reviews Section -->
<h2>Отзывы ({{ item()!.callbacks?.length || 0 }})</h2> <div class="dx-reviews-section">
<h2>{{ 'itemDetail.reviews' | translate }} ({{ item()!.callbacks?.length || 0 }})</h2>
<!-- Review Submission Form --> <div class="dx-review-form">
<div class="review-form"> <h3>{{ 'itemDetail.leaveReview' | translate }}</h3>
<h3>Оставить отзыв</h3> <div class="dx-rating-input">
<div class="rating-input"> <label>{{ 'itemDetail.rating' | translate }}</label>
<label>Оценка:</label> <div class="dx-star-selector">
<div class="star-selector">
@for (star of [1, 2, 3, 4, 5]; track star) { @for (star of [1, 2, 3, 4, 5]; track star) {
<span <span
class="star" class="dx-star-pick"
[class.selected]="newReview.rating >= star" [class.selected]="newReview.rating >= star"
(click)="setRating(star)"> (click)="setRating(star)">
</span> </span>
} }
</div> </div>
</div> </div>
<textarea <textarea
[(ngModel)]="newReview.comment" [(ngModel)]="newReview.comment"
placeholder="Напишите свой отзыв..." [placeholder]="'itemDetail.reviewPlaceholderDexar' | translate"
rows="4"> rows="4"
class="dx-textarea">
</textarea> </textarea>
<div class="form-actions"> <div class="dx-form-actions">
<label class="anonymous-toggle"> <label class="dx-anon-toggle">
<input type="checkbox" [(ngModel)]="newReview.anonymous"> <input type="checkbox" [(ngModel)]="newReview.anonymous">
Анонимно <span>{{ 'itemDetail.anonymous' | translate }}</span>
</label> </label>
@if (!newReview.anonymous && getUserDisplayName()) { @if (!newReview.anonymous && getUserDisplayName()) {
<span class="username-preview">Опубликуется как: {{ getUserDisplayName() }}</span> <span class="dx-user-preview">{{ getUserDisplayName() }}</span>
} }
<button <button
class="submit-review-btn" class="dx-submit-btn"
(click)="submitReview()" (click)="submitReview()"
[disabled]="!newReview.rating || !newReview.comment.trim() || reviewSubmitStatus() === 'loading'" [disabled]="!newReview.rating || !newReview.comment.trim() || reviewSubmitStatus() === 'loading'"
[class.submitting]="reviewSubmitStatus() === 'loading'"> [class.submitting]="reviewSubmitStatus() === 'loading'">
@if (reviewSubmitStatus() === 'loading') { @if (reviewSubmitStatus() === 'loading') {
<span class="spinner-small"></span> <span class="dx-spinner-sm"></span>
Отправка... {{ 'itemDetail.submitting' | translate }}
} @else { } @else {
Отправить отзыв {{ 'itemDetail.submit' | translate }}
} }
</button> </button>
</div> </div>
@if (reviewSubmitStatus() === 'success') { @if (reviewSubmitStatus() === 'success') {
<div class="status-message success"> <div class="dx-status success">
<span class="icon"></span> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
Спасибо за ваш отзыв! {{ 'itemDetail.reviewSuccess' | translate }}
</div> </div>
} }
@if (reviewSubmitStatus() === 'error') { @if (reviewSubmitStatus() === 'error') {
<div class="status-message error"> <div class="dx-status error">
<span class="icon"></span> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
Ошибка отправки. Попробуйте позже. {{ 'itemDetail.reviewError' | translate }}
</div> </div>
} }
</div> </div>
<div class="reviews-list"> <div class="dx-reviews-list">
@if (item()?.callbacks && item()!.callbacks!.length > 0) { @if (item()?.callbacks && item()!.callbacks!.length > 0) {
@for (callback of item()!.callbacks; track $index) { @for (callback of item()!.callbacks; track $index) {
<div class="review-card"> <div class="dx-review-card">
<div class="review-header"> <div class="dx-review-header">
<span class="review-author">{{ callback.userID || 'Аноним' }}</span> <div class="dx-reviewer">
@if (callback.rating) { <span class="dx-reviewer-name">{{ callback.userID ? callback.userID : ('itemDetail.defaultUser' | translate) }}</span>
<div class="review-rating">{{ getRatingStars(callback.rating) }}</div>
}
@if (callback.timestamp) { @if (callback.timestamp) {
<span class="review-date">{{ formatDate(callback.timestamp) }}</span> <span class="dx-review-date">{{ formatDate(callback.timestamp) }}</span>
}
</div>
@if (callback.rating) {
<div class="dx-review-stars">
@for (star of [1, 2, 3, 4, 5]; track star) {
<svg width="14" height="14" viewBox="0 0 24 24" [attr.fill]="star <= callback.rating ? '#497671' : 'none'" [attr.stroke]="star <= callback.rating ? '#497671' : '#d3dad9'" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
}
</div>
} }
</div> </div>
@if (callback.content) { @if (callback.content) {
<p class="review-text">{{ callback.content }}</p> <p class="dx-review-text">{{ callback.content }}</p>
} }
</div> </div>
} }
} @else { } @else {
<p class="no-reviews">Пока нет отзывов</p> <p class="dx-no-reviews">{{ 'itemDetail.noReviews' | translate }}</p>
} }
</div> </div>
</div> </div>
<div class="questions-section"> <!-- Q&A Section -->
<h2>Вопросы и ответы ({{ item()!.questions?.length || 0 }})</h2> @if (item()!.questions && item()!.questions!.length > 0) {
<div class="questions-list"> <div class="dx-qa-section">
@if (item()?.questions && item()!.questions!.length > 0) { <h2>{{ 'itemDetail.qna' | translate }} ({{ item()!.questions!.length }})</h2>
@for (question of item()!.questions; track $index) { <div class="dx-qa-list">
<div class="question-card"> @for (question of item()!.questions!; track $index) {
<div class="question-text"> <div class="dx-qa-card">
<strong>В:</strong> {{ question.question }} <div class="dx-question">
<span class="dx-qa-label q">В</span>
<span>{{ question.question }}</span>
</div> </div>
<div class="answer-text"> <div class="dx-answer">
<strong>О:</strong> {{ question.answer }} <span class="dx-qa-label a">О</span>
<span>{{ question.answer }}</span>
</div> </div>
<div class="question-votes"> <div class="dx-qa-votes">
<span class="vote-up">👍 {{ question.upvotes }}</span> <button class="dx-vote up">
<span class="vote-down">👎 {{ question.downvotes }}</span> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>
{{ question.upvotes }}
</button>
<button class="dx-vote down">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"/><path d="M17 2h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/></svg>
{{ question.downvotes }}
</button>
</div> </div>
</div> </div>
} }
} @else { </div>
<p class="no-questions">Пока нет вопросов</p> </div>
} }
</div>
</div>
} }
</div> </div>
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,21 @@
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy } from '@angular/core'; import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService, CartService, TelegramService } from '../../services'; import { ApiService, CartService, TelegramService, SeoService } from '../../services';
import { Item } from '../../models'; import { Item } from '../../models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { SecurityContext } from '@angular/core';
import { getDiscountedPrice } from '../../utils/item.utils';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service';
@Component({ @Component({
selector: 'app-item-detail', selector: 'app-item-detail',
standalone: true, imports: [DecimalPipe, RouterLink, FormsModule, LangRoutePipe, TranslatePipe],
imports: [CommonModule, RouterLink, FormsModule],
templateUrl: './item-detail.component.html', templateUrl: './item-detail.component.html',
styleUrls: ['./item-detail.component.scss'], styleUrls: ['./item-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@@ -32,6 +36,12 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
reviewSubmitStatus = signal<'idle' | 'loading' | 'success' | 'error'>('idle'); reviewSubmitStatus = signal<'idle' | 'loading' | 'success' | 'error'>('idle');
private routeSubscription?: Subscription; private routeSubscription?: Subscription;
private reviewResetTimeout?: ReturnType<typeof setTimeout>;
private reviewErrorTimeout?: ReturnType<typeof setTimeout>;
private reloadTimeout?: ReturnType<typeof setTimeout>;
private seoService = inject(SeoService);
private i18n = inject(TranslateService);
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -50,6 +60,10 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.routeSubscription?.unsubscribe(); this.routeSubscription?.unsubscribe();
if (this.reviewResetTimeout) clearTimeout(this.reviewResetTimeout);
if (this.reviewErrorTimeout) clearTimeout(this.reviewErrorTimeout);
if (this.reloadTimeout) clearTimeout(this.reloadTimeout);
this.seoService.resetToDefaults();
} }
loadItem(itemID: number): void { loadItem(itemID: number): void {
@@ -58,6 +72,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
this.apiService.getItem(itemID).subscribe({ this.apiService.getItem(itemID).subscribe({
next: (item) => { next: (item) => {
this.item.set(item); this.item.set(item);
this.seoService.setItemMeta(item);
this.loading.set(false); this.loading.set(false);
}, },
error: (err) => { error: (err) => {
@@ -82,18 +97,18 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
getDiscountedPrice(): number { getDiscountedPrice(): number {
const currentItem = this.item(); const currentItem = this.item();
if (!currentItem) return 0; if (!currentItem) return 0;
return currentItem.price * (1 - currentItem.discount / 100); return getDiscountedPrice(currentItem);
} }
getSafeHtml(html: string): SafeHtml { getSafeHtml(html: string): SafeHtml {
return this.sanitizer.sanitize(1, html) || ''; return this.sanitizer.sanitize(SecurityContext.HTML, html) || '';
} }
getRatingStars(rating: number): string { getRatingStars(rating: number): string {
const fullStars = Math.floor(rating); const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 >= 0.5; const hasHalfStar = rating % 1 >= 0.5;
let stars = ''.repeat(fullStars); let stars = ''.repeat(fullStars);
if (hasHalfStar) stars += ''; if (hasHalfStar) stars += '';
return stars; return stars;
} }
@@ -103,10 +118,10 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
const diffMs = now.getTime() - date.getTime(); const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) return 'Сегодня'; if (diffDays === 0) return this.i18n.t('itemDetail.today');
if (diffDays === 1) return 'Вчера'; if (diffDays === 1) return this.i18n.t('itemDetail.yesterday');
if (diffDays < 7) return `${diffDays} дн. назад`; if (diffDays < 7) return `${diffDays} ${this.i18n.t('itemDetail.daysAgo')}`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)} нед. назад`; if (diffDays < 30) return `${Math.floor(diffDays / 7)} ${this.i18n.t('itemDetail.weeksAgo')}`;
return date.toLocaleDateString('ru-RU', { return date.toLocaleDateString('ru-RU', {
day: 'numeric', day: 'numeric',
@@ -121,7 +136,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
getUserDisplayName(): string | null { getUserDisplayName(): string | null {
if (!this.telegramService.isTelegramApp()) { if (!this.telegramService.isTelegramApp()) {
return 'Пользователь'; return this.i18n.t('itemDetail.defaultUser');
} }
return this.telegramService.getDisplayName(); return this.telegramService.getDisplayName();
} }
@@ -150,13 +165,13 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
this.reviewSubmitStatus.set('success'); this.reviewSubmitStatus.set('success');
this.newReview = { rating: 0, comment: '', anonymous: false }; this.newReview = { rating: 0, comment: '', anonymous: false };
// Скрыть сообщение через 3 секунды // Сброс состояния через 3 секунды
setTimeout(() => { this.reviewResetTimeout = setTimeout(() => {
this.reviewSubmitStatus.set('idle'); this.reviewSubmitStatus.set('idle');
}, 3000); }, 3000);
// Перезагрузить данные товара через небольшую задержку // Перезагрузить данные товара после отправки отзыва
setTimeout(() => { this.reloadTimeout = setTimeout(() => {
this.loadItem(currentItem.itemID); this.loadItem(currentItem.itemID);
}, 500); }, 500);
}, },
@@ -164,8 +179,8 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
console.error('Error submitting review:', err); console.error('Error submitting review:', err);
this.reviewSubmitStatus.set('error'); this.reviewSubmitStatus.set('error');
// Скрыть сообщение об ошибке через 5 секунд // Сброс состояния об ошибке через 5 секунд
setTimeout(() => { this.reviewErrorTimeout = setTimeout(() => {
this.reviewSubmitStatus.set('idle'); this.reviewSubmitStatus.set('idle');
}, 5000); }, 5000);
} }

View File

@@ -1,13 +1,12 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
@Component({ @Component({
selector: 'app-company-details', selector: 'app-company-details',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './company-details.component.html', templateUrl: './company-details.component.html',
styleUrls: ['./company-details.component.scss'] styleUrls: ['./company-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class CompanyDetailsComponent { export class CompanyDetailsComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -105,7 +105,7 @@
<section class="info-card wide"> <section class="info-card wide">
<div class="card-icon">↩️</div> <div class="card-icon">↩️</div>
<h2>6. Возврат средств</h2> <h2>6. Возврат средств</h2>
<p>6.1. Порядок возврата денежных средств регулируется <a routerLink="/return-policy">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p> <p>6.1. Порядок возврата денежных средств регулируется <a [routerLink]="'/return-policy' | langRoute">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p>
<p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p> <p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p>
<p>6.3. Срок возврата денежных средств составляет:</p> <p>6.3. Срок возврата денежных средств составляет:</p>
<div class="refund-times"> <div class="refund-times">
@@ -232,7 +232,7 @@
<section class="legal-section"> <section class="legal-section">
<h2>6. Возврат средств</h2> <h2>6. Возврат средств</h2>
<p>6.1. Порядок возврата денежных средств регулируется <a routerLink="/return-policy">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p> <p>6.1. Порядок возврата денежных средств регулируется <a [routerLink]="'/return-policy' | langRoute">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p>
<p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p> <p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p>
<p>6.3. Срок возврата денежных средств составляет:</p> <p>6.3. Срок возврата денежных средств составляет:</p>
<ul> <ul>

View File

@@ -1,14 +1,14 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { LangRoutePipe } from '../../../pipes/lang-route.pipe';
@Component({ @Component({
selector: 'app-payment-terms', selector: 'app-payment-terms',
standalone: true, imports: [RouterLink, LangRoutePipe],
imports: [CommonModule, RouterLink],
templateUrl: './payment-terms.component.html', templateUrl: './payment-terms.component.html',
styleUrls: ['./payment-terms.component.scss'] styleUrls: ['./payment-terms.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PaymentTermsComponent { export class PaymentTermsComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -1,13 +1,12 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
@Component({ @Component({
selector: 'app-privacy-policy', selector: 'app-privacy-policy',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './privacy-policy.component.html', templateUrl: './privacy-policy.component.html',
styleUrls: ['./privacy-policy.component.scss'] styleUrls: ['./privacy-policy.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PrivacyPolicyComponent { export class PrivacyPolicyComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -33,7 +33,7 @@
<p>1.4. Акцепт происходит автоматически при любом действии: визите, регистрации, оформлении покупки.</p> <p>1.4. Акцепт происходит автоматически при любом действии: визите, регистрации, оформлении покупки.</p>
<p>1.5. Подписание бумажного договора не требуется — электронная форма юридически действительна.</p> <p>1.5. Подписание бумажного договора не требуется — электронная форма юридически действительна.</p>
<p>1.6. Несогласие с условиями означает обязанность покинуть сайт.</p> <p>1.6. Несогласие с условиями означает обязанность покинуть сайт.</p>
<p>1.7. Также применяется <a routerLink="/privacy-policy">Политика конфиденциальности</a>.</p> <p>1.7. Также применяется <a [routerLink]="'/privacy-policy' | langRoute">Политика конфиденциальности</a>.</p>
<p>1.8. Мы можем обновлять условия в одностороннем порядке.</p> <p>1.8. Мы можем обновлять условия в одностороннем порядке.</p>
<p>1.9. Промо-кампании могут иметь специальные правила.</p> <p>1.9. Промо-кампании могут иметь специальные правила.</p>
</section> </section>
@@ -77,7 +77,7 @@
<ul class="compact-list"> <ul class="compact-list">
<li>Устанавливать ограничения в использовании ресурса</li> <li>Устанавливать ограничения в использовании ресурса</li>
<li>Направлять информацию о нововведениях</li> <li>Направлять информацию о нововведениях</li>
<li>Передавать полномочия третьим лицам</li> <li>Изменять поставщика товаров и/или компанию-доставщика без предварительного согласия Покупателя</li>
<li>Изменять условия акций в одностороннем порядке</li> <li>Изменять условия акций в одностороннем порядке</li>
<li>Ограничивать действия пользователей, создающих риски</li> <li>Ограничивать действия пользователей, создающих риски</li>
<li>Проводить технические работы без предупреждения</li> <li>Проводить технические работы без предупреждения</li>
@@ -136,17 +136,15 @@
<section class="info-card wide"> <section class="info-card wide">
<div class="card-icon">⚖️</div> <div class="card-icon">⚖️</div>
<h2>9. Ответственность сторон</h2> <h2>9. Ответственность сторон</h2>
<p><strong>9.1. Общее:</strong> Пользователь использует сайт на свой риск. Ресурсы предоставляются "как есть".</p> <p><strong>9.1. Общее:</strong> Сайт является информационно-технологическим ресурсом, обеспечивающим размещение информации о товарах и услугах, а также безопасность совершения сделок. Владелец сайта принимает на себя ответственность за недопущение размещения запрещённых товаров на ресурсе, а также за качество и достоверность информации о товарах и услугах, представленных на платформе.</p>
<p><strong>9.2. Ограничения:</strong> Владелец не гарантирует постоянную доступность и отсутствие ошибок.</p> <p><strong>9.2. Ограничения:</strong> Владелец не гарантирует постоянную доступность и отсутствие ошибок.</p>
<p><strong>9.3. Информация:</strong> Пользователь несёт ответственность за последствия использования информации.</p> <p><strong>9.3. Информация:</strong> Пользователь несёт ответственность за последствия использования информации.</p>
<p><strong>9.4. Недостоверные данные:</strong> Владелец не отвечает за неверные данные пользователя.</p> <p><strong>9.4. Идентификация и контроль:</strong> Покупатель идентифицируется по номеру телефона и данным, предоставляемым через Telegram. Каждый Продавец проходит полную процедуру идентификации (онбординг), его данные доступны при возникновении спорных ситуаций. Владелец сайта осуществляет проверку и контроль деятельности Продавцов на платформе.</p>
<p><strong>9.5. Безопасность аккаунта:</strong> Пользователь отвечает за сохранность логина и пароля.</p> <p><strong>9.5. Безопасность аккаунта:</strong> Пользователь отвечает за сохранность логина и пароля.</p>
<p><strong>9.6. Ответственность продавца:</strong> Продавец отвечает за качество, безопасность и соответствие товаров.</p> <p><strong>9.6. Ответственность продавца:</strong> Продавец отвечает за качество, безопасность и соответствие товаров.</p>
<p><strong>9.7. Освобождение владельца:</strong> Владелец не отвечает за:</p> <p><strong>9.7. Ответственность владельца:</strong> Владелец несёт ответственность за качество, безопасность и достоверность информации о товарах и услугах на платформе. При этом Владелец не отвечает за:</p>
<ul class="compact-list"> <ul class="compact-list">
<li>Качество, безопасность и правомерность товаров продавцов</li>
<li>Выполнение продавцами обязательств</li> <li>Выполнение продавцами обязательств</li>
<li>Истинность информации о товарах</li>
<li>Нарушение прав третьих лиц</li> <li>Нарушение прав третьих лиц</li>
<li>Доставку, комплектацию и состояние товаров</li> <li>Доставку, комплектацию и состояние товаров</li>
</ul> </ul>
@@ -208,7 +206,7 @@
<section class="info-card wide"> <section class="info-card wide">
<div class="card-icon">↩️</div> <div class="card-icon">↩️</div>
<h2>13. Возврат и обмен товара</h2> <h2>13. Возврат и обмен товара</h2>
<p><strong>13.1. Общие правила:</strong> Цифровые товары не подлежат возврату. Физические товары — согласно <a routerLink="/return-policy">Политике возврата</a> и законам о правах потребителей.</p> <p><strong>13.1. Общие правила:</strong> Цифровые товары не подлежат возврату. Физические товары — согласно <a [routerLink]="'/return-policy' | langRoute">Политике возврата</a> и законам о правах потребителей.</p>
<p><strong>13.2. Процедура возврата:</strong> В соответствии с соглашением и законодательством РФ.</p> <p><strong>13.2. Процедура возврата:</strong> В соответствии с соглашением и законодательством РФ.</p>
<p><strong>13.3. Акционные наборы:</strong> Возврат только в комплексе, отдельные товары вернуть нельзя.</p> <p><strong>13.3. Акционные наборы:</strong> Возврат только в комплексе, отдельные товары вернуть нельзя.</p>
<p><strong>13.4. Затраты на доставку:</strong> При возврате качественного товара продавец может взыскать затраты на доставку.</p> <p><strong>13.4. Затраты на доставку:</strong> При возврате качественного товара продавец может взыскать затраты на доставку.</p>
@@ -318,7 +316,7 @@
<p>1.7. В случае несогласия с условиями Пользователь обязуется незамедлительно прекратить пользование ресурсом.</p> <p>1.7. В случае несогласия с условиями Пользователь обязуется незамедлительно прекратить пользование ресурсом.</p>
<p>1.8. Дополнительно регулирование использования сайта осуществляется <a routerLink="/privacy-policy">Политикой обработки персональных данных</a>.</p> <p>1.8. Дополнительно регулирование использования сайта осуществляется <a [routerLink]="'/privacy-policy' | langRoute">Политикой обработки персональных данных</a>.</p>
<p>1.9. Изменения в соглашение могут вноситься Владельцем без предварительного уведомления и становятся обязательными с момента публикации изменений.</p> <p>1.9. Изменения в соглашение могут вноситься Владельцем без предварительного уведомления и становятся обязательными с момента публикации изменений.</p>
@@ -398,7 +396,7 @@
<ul> <ul>
<li>Устанавливать ограничения в использовании ресурса для всех или отдельных групп пользователей.</li> <li>Устанавливать ограничения в использовании ресурса для всех или отдельных групп пользователей.</li>
<li>Направлять Пользователям информацию о нововведениях и изменениях на сайте.</li> <li>Направлять Пользователям информацию о нововведениях и изменениях на сайте.</li>
<li>Передавать полномочия третьим лицам без одобрения Покупателя.</li> <li>Изменять поставщика товаров и/или компанию-доставщика без предварительного согласия Покупателя.</li>
<li>Изменять условия проводимых акций в одностороннем порядке.</li> <li>Изменять условия проводимых акций в одностороннем порядке.</li>
<li>Ограничивать действия Пользователей, создающих риски для работоспособности сайта.</li> <li>Ограничивать действия Пользователей, создающих риски для работоспособности сайта.</li>
<li>Проводить технические работы без предупреждения пользователей.</li> <li>Проводить технические работы без предупреждения пользователей.</li>
@@ -487,7 +485,7 @@
<h2>9. Ответственность сторон</h2> <h2>9. Ответственность сторон</h2>
<p><strong>9.1. Общая ответственность</strong></p> <p><strong>9.1. Общая ответственность</strong></p>
<p>Пользователь использует сайт на свой риск. Ресурсы предоставляются "как есть", без гарантий соответствия ожиданиям пользователя.</p> <p>Сайт является информационно-технологическим ресурсом, обеспечивающим размещение информации о товарах и услугах, а также безопасность совершения сделок. Владелец сайта принимает на себя ответственность за недопущение размещения запрещённых товаров на ресурсе, а также за качество и достоверность информации о товарах и услугах, представленных на платформе.</p>
<p><strong>9.2. Ограничения ответственности</strong></p> <p><strong>9.2. Ограничения ответственности</strong></p>
<p>Владелец сайта не гарантирует:</p> <p>Владелец сайта не гарантирует:</p>
@@ -499,8 +497,8 @@
<p><strong>9.3. Последствия использования информации</strong></p> <p><strong>9.3. Последствия использования информации</strong></p>
<p>Вся информация и материалы, получаемые через сайт, используются пользователем на его усмотрение и риск. Пользователь несёт ответственность за возможные негативные последствия использования этих сведений.</p> <p>Вся информация и материалы, получаемые через сайт, используются пользователем на его усмотрение и риск. Пользователь несёт ответственность за возможные негативные последствия использования этих сведений.</p>
<p><strong>9.4. Ответственность за недостоверную информацию</strong></p> <p><strong>9.4. Идентификация участников и контроль Продавцов</strong></p>
<p>Владелец сайта не несёт ответственности за неверные или неточные данные, предоставленные пользователем при регистрации.</p> <p>Покупатель идентифицируется на основании номера телефона и данных, предоставляемых через Telegram. Каждый Продавец проходит полную процедуру идентификации (онбординг), и его данные доступны Владельцу сайта для использования при возникновении спорных ситуаций. Владелец сайта осуществляет проверку и контроль деятельности Продавцов на платформе.</p>
<p><strong>9.5. Безопасность аккаунта</strong></p> <p><strong>9.5. Безопасность аккаунта</strong></p>
<p>Пользователь самостоятельно отвечает за сохранение секретности используемых средств доступа к своему профилю.</p> <p>Пользователь самостоятельно отвечает за сохранение секретности используемых средств доступа к своему профилю.</p>
@@ -508,16 +506,14 @@
<p><strong>9.6. Ответственность Продавца</strong></p> <p><strong>9.6. Ответственность Продавца</strong></p>
<p>Продавец несёт полную ответственность за качество, безопасность и соответствие реализуемой продукции заявленным характеристикам, а также за убытки, вызванные нарушением обязательств перед Покупателем.</p> <p>Продавец несёт полную ответственность за качество, безопасность и соответствие реализуемой продукции заявленным характеристикам, а также за убытки, вызванные нарушением обязательств перед Покупателем.</p>
<p><strong>9.7. Освобождение Владельца сайта от ответственности</strong></p> <p><strong>9.7. Ответственность Владельца сайта и порядок урегулирования претензий</strong></p>
<p>Владелец сайта не несёт ответственности за:</p> <p>Владелец сайта несёт ответственность за качество, безопасность и достоверность информации о товарах и услугах, размещённых на платформе. При этом Владелец сайта не несёт ответственности за:</p>
<ul> <ul>
<li>Качество, безопасность и правомерность товаров и услуг, предлагаемых Продавцами.</li>
<li>Выполнение или ненадлежащее выполнение Продавцами своих обязательств перед Покупателями.</li> <li>Выполнение или ненадлежащее выполнение Продавцами своих обязательств перед Покупателями.</li>
<li>Истинность информации, представляемой Продавцами о товаре.</li>
<li>Повреждение прав третьих лиц, включая интеллектуальную собственность.</li> <li>Повреждение прав третьих лиц, включая интеллектуальную собственность.</li>
<li>Вопросы доставки, комплектации и состояния товаров.</li> <li>Вопросы доставки, комплектации и состояния товаров.</li>
</ul> </ul>
<p>Покупатель соглашается, что претензии по качеству, количеству, комплектности товаров и услугам следует направлять непосредственно Продавцу. Администрация сайта может способствовать решению споров, но сама ответственности не несёт.</p> <p>Покупатель соглашается, что претензии по качеству, количеству, комплектности товаров и услугам могут быть направлены как Продавцу, так и Администрации сайта. Администрация сайта принимает на себя ответственность за качество и достоверность услуг, предоставляемых через платформу, и активно участвует в урегулировании спорных ситуаций.</p>
<p><strong>9.8. Ответственность за доставку</strong></p> <p><strong>9.8. Ответственность за доставку</strong></p>
<p>Ответственность за сроки, условия и качество доставки товаров несут транспортные компании и курьерские службы. Владелец сайта выступает только в качестве информационного посредника и не несёт ответственности за действия служб доставки.</p> <p>Ответственность за сроки, условия и качество доставки товаров несут транспортные компании и курьерские службы. Владелец сайта выступает только в качестве информационного посредника и не несёт ответственности за действия служб доставки.</p>
@@ -644,7 +640,7 @@
<h2>13. Возврат и обмен товара</h2> <h2>13. Возврат и обмен товара</h2>
<p><strong>13.1. Общие правила возврата</strong></p> <p><strong>13.1. Общие правила возврата</strong></p>
<p>Цифровые товары (предоставляемые в электронной форме) не подлежат возврату согласно российскому законодательству. Возврат физических товаров осуществляется в соответствии с разделом <a routerLink="/return-policy">«Политика возврата»</a> и действующими законами о правах потребителей.</p> <p>Цифровые товары (предоставляемые в электронной форме) не подлежат возврату согласно российскому законодательству. Возврат физических товаров осуществляется в соответствии с разделом <a [routerLink]="'/return-policy' | langRoute">«Политика возврата»</a> и действующими законами о правах потребителей.</p>
<p><strong>13.2. Возврат товара</strong></p> <p><strong>13.2. Возврат товара</strong></p>
<p>Возврат или замена товаров, представленных на сайте и подлежащих возврату, происходят в соответствии с данным соглашением и законодательством Российской Федерации.</p> <p>Возврат или замена товаров, представленных на сайте и подлежащих возврату, происходят в соответствии с данным соглашением и законодательством Российской Федерации.</p>

View File

@@ -1,14 +1,14 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { LangRoutePipe } from '../../../pipes/lang-route.pipe';
@Component({ @Component({
selector: 'app-public-offer', selector: 'app-public-offer',
standalone: true, imports: [RouterLink, LangRoutePipe],
imports: [CommonModule, RouterLink],
templateUrl: './public-offer.component.html', templateUrl: './public-offer.component.html',
styleUrls: ['./public-offer.component.scss'] styleUrls: ['./public-offer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PublicOfferComponent { export class PublicOfferComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -1,14 +1,12 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
@Component({ @Component({
selector: 'app-return-policy', selector: 'app-return-policy',
standalone: true, imports: [],
imports: [CommonModule],
templateUrl: './return-policy.component.html', templateUrl: './return-policy.component.html',
styleUrls: ['./return-policy.component.scss'] styleUrls: ['./return-policy.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReturnPolicyComponent { export class ReturnPolicyComponent {
brandName = environment.brandName; brandName = environment.brandName;

View File

@@ -1,45 +1,55 @@
<div class="search-container"> <div class="search-container">
<div class="search-header"> <div class="search-header">
<h1>Поиск товаров</h1> <h1>{{ 'search.title' | translate }}</h1>
<div class="search-box"> <div class="search-box">
<input <input
type="text" type="text"
[(ngModel)]="searchQuery" [(ngModel)]="searchQuery"
(input)="onSearchInput(searchQuery)" (input)="onSearchInput(searchQuery)"
placeholder="Введите название товара..." [placeholder]="'search.placeholder' | translate"
class="search-input" class="search-input"
autofocus autofocus
/> />
<span class="search-icon">🔍</span> <span class="search-icon">
<svg width="22" height="22" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" fill="#697777" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#697777" />
</svg>
</span>
</div> </div>
</div> </div>
@if (searchQuery && items().length > 0) { @if (searchQuery && items().length > 0) {
<div class="results-count"> <div class="results-count">
Найдено товаров: {{ items().length }}@if (totalResults() > items().length) { из {{ totalResults() }} } {{ 'search.resultsCount' | translate }} {{ items().length }}@if (totalResults() > items().length) { {{ 'search.of' | translate }} {{ totalResults() }} }
</div> </div>
} }
@if (loading() && items().length === 0) { @if (loading() && items().length === 0) {
<div class="loading-initial"> <div class="loading-initial">
<div class="spinner"></div> <div class="spinner"></div>
<p>Поиск...</p> <p>{{ 'search.searching' | translate }}</p>
</div> </div>
} }
@if (error()) { @if (error()) {
<div class="error"> <div class="error">
<p>{{ error() }}</p> <p>{{ error() }}</p>
<button (click)="performSearch(searchQuery)">Попробовать снова</button> <button (click)="performSearch(searchQuery)">{{ 'search.retry' | translate }}</button>
</div> </div>
} }
@if (!loading() && searchQuery && items().length === 0 && !error()) { @if (!loading() && searchQuery && items().length === 0 && !error()) {
<div class="no-results"> <div class="no-results">
<div class="no-results-icon">🔍</div> <div class="no-results-icon">
<h2>Ничего не найдено</h2> <svg width="48" height="48" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<p>По запросу "{{ searchQuery }}" товары не найдены</p> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZZ2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" fill="#a1b4b5" />
<p class="hint">Попробуйте изменить запрос или используйте другие ключевые слова</p> <path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#a1b4b5" />
</svg>
</div>
<h2>{{ 'search.noResults' | translate }}</h2>
<p>{{ 'search.noResultsFor' | translate:{ query: searchQuery } }}</p>
<p class="hint">{{ 'search.noResultsHint' | translate }}</p>
</div> </div>
} }
@@ -47,7 +57,7 @@
<div class="items-grid"> <div class="items-grid">
@for (item of items(); track trackByItemId($index, item)) { @for (item of items(); track trackByItemId($index, item)) {
<div class="item-card"> <div class="item-card">
<a [routerLink]="['/item', item.itemID]" class="item-link"> <a [routerLink]="['/item', item.itemID] | langRoute" class="item-link">
<div class="item-image"> <div class="item-image">
<img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" decoding="async" width="300" height="300" /> <img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" decoding="async" width="300" height="300" />
@if (item.discount > 0) { @if (item.discount > 0) {
@@ -85,7 +95,7 @@
</a> </a>
<button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)"> <button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)">
В корзину {{ 'search.addToCart' | translate }}
</button> </button>
</div> </div>
} }
@@ -94,20 +104,26 @@
@if (loading() && items().length > 0) { @if (loading() && items().length > 0) {
<div class="loading-more"> <div class="loading-more">
<div class="spinner"></div> <div class="spinner"></div>
<p>Загрузка...</p> <p>{{ 'search.loadingMore' | translate }}</p>
</div> </div>
} }
@if (!hasMore() && items().length > 0) { @if (!hasMore() && items().length > 0) {
<div class="no-more"> <div class="no-more">
<p>Все результаты загружены</p> <p>{{ 'search.allLoaded' | translate }}</p>
</div> </div>
} }
} }
@if (!searchQuery) { @if (!searchQuery) {
<div class="empty-state"> <div class="empty-state">
<p>Введите запрос для поиска товаров</p> <div class="empty-state-icon">
<svg width="56" height="56" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12Z" fill="#d3dad9" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#d3dad9" />
</svg>
</div>
<p>{{ 'search.emptyState' | translate }}</p>
</div> </div>
} }
</div> </div>

View File

@@ -2,6 +2,7 @@
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
} }
.search-header { .search-header {
@@ -10,7 +11,7 @@
h1 { h1 {
font-size: 2rem; font-size: 2rem;
margin-bottom: 20px; margin-bottom: 20px;
color: #333; color: #1e3c38;
text-align: center; text-align: center;
} }
} }
@@ -24,13 +25,15 @@
width: 100%; width: 100%;
padding: 16px 50px 16px 20px; padding: 16px 50px 16px 20px;
font-size: 1.1rem; font-size: 1.1rem;
border: 2px solid #ddd; font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
border-radius: 50px; border: 2px solid #d3dad9;
border-radius: 13px;
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s;
&:focus { &:focus {
border-color: var(--primary-color); border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.1);
} }
} }
@@ -39,7 +42,8 @@
right: 20px; right: 20px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
font-size: 1.5rem; display: flex;
align-items: center;
pointer-events: none; pointer-events: none;
} }
} }
@@ -47,40 +51,76 @@
.results-count { .results-count {
text-align: center; text-align: center;
margin-bottom: 24px; margin-bottom: 24px;
color: #333; color: #1e3c38;
font-size: 1.1rem; font-size: 1.1rem;
} }
.empty-state, .empty-state,
.no-results,
.loading-initial { .loading-initial {
text-align: center; text-align: center;
padding: 60px 20px; padding: 60px 20px;
color: #555; color: #697777;
} }
.no-results { .empty-state {
.no-results-icon { display: flex;
font-size: 4rem; flex-direction: column;
margin-bottom: 20px; align-items: center;
opacity: 0.3; padding: 80px 20px;
}
h2 { .empty-state-icon {
font-size: 1.5rem; width: 90px;
color: #333; height: 90px;
margin-bottom: 12px; display: flex;
align-items: center;
justify-content: center;
background: #f5f3f9;
border-radius: 50%;
margin-bottom: 20px;
} }
p { p {
margin: 8px 0; font-size: 1rem;
color: #666; color: #a1b4b5;
margin: 0;
}
}
.no-results {
text-align: center;
padding: 80px 20px;
display: flex;
flex-direction: column;
align-items: center;
.no-results-icon {
width: 90px;
height: 90px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f3f9;
border-radius: 50%;
margin-bottom: 20px;
}
h2 {
font-size: 1.25rem;
font-weight: 700;
color: #1e3c38;
margin: 0 0 8px 0;
}
p {
margin: 4px 0;
color: #697777;
font-size: 0.95rem;
} }
.hint { .hint {
font-size: 0.9rem; font-size: 0.875rem;
color: #999; color: #a1b4b5;
margin-top: 16px; margin-top: 12px;
} }
} }
@@ -94,9 +134,10 @@
background: var(--primary-color); background: var(--primary-color);
color: white; color: white;
border: none; border: none;
border-radius: 6px; border-radius: 13px;
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
&:hover { &:hover {
background: var(--primary-hover); background: var(--primary-hover);
@@ -113,8 +154,9 @@
.item-card { .item-card {
background: white; background: white;
border-radius: 12px; border-radius: 13px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid #d3dad9;
box-shadow: 0 3px 4px rgba(0, 0, 0, 0.08);
overflow: hidden; overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
display: flex; display: flex;
@@ -122,7 +164,7 @@
&:hover { &:hover {
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
} }
} }
@@ -140,6 +182,7 @@
padding-top: 75%; padding-top: 75%;
background: #f5f5f5; background: #f5f5f5;
overflow: hidden; overflow: hidden;
border-radius: 13px;
img { img {
position: absolute; position: absolute;
@@ -149,6 +192,14 @@
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
background: white; background: white;
padding: 12px;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), filter 0.4s ease;
filter: drop-shadow(0 2px 6px rgba(0,0,0,0.06));
}
&:hover img {
transform: scale(1.06) translateY(-2px);
filter: drop-shadow(0 12px 24px rgba(0,0,0,0.15)) brightness(1.03);
} }
} }
@@ -156,7 +207,7 @@
position: absolute; position: absolute;
top: 12px; top: 12px;
right: 12px; right: 12px;
background: #ff4757; background: #ef4444;
color: white; color: white;
padding: 6px 12px; padding: 6px 12px;
border-radius: 20px; border-radius: 20px;
@@ -175,7 +226,7 @@
.item-name { .item-name {
font-size: 1.1rem; font-size: 1.1rem;
margin: 0; margin: 0;
color: #333; color: #1e3c38;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2; line-clamp: 2;
@@ -188,10 +239,10 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 0.9rem; font-size: 0.9rem;
color: #333; color: #1e3c38;
.rating-stars { .rating-stars {
color: #ffa502; color: #497671;
font-weight: 600; font-weight: 600;
} }
} }
@@ -206,7 +257,7 @@
.original-price { .original-price {
text-decoration: line-through; text-decoration: line-through;
color: #555; color: #697777;
font-size: 0.9rem; font-size: 0.9rem;
} }
@@ -226,7 +277,7 @@
.bar-segment { .bar-segment {
width: 20px; width: 20px;
height: 6px; height: 6px;
background: #e0e0e0; background: #d3dad9;
border-radius: 3px; border-radius: 3px;
transition: background 0.2s; transition: background 0.2s;
@@ -239,7 +290,7 @@
} }
&.filled.low { &.filled.low {
background: #ff4757; background: #ef4444;
} }
} }
} }
@@ -251,8 +302,10 @@
background: var(--primary-color); background: var(--primary-color);
color: white; color: white;
border: none; border: none;
border-radius: 0 0 13px 13px;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
@@ -286,7 +339,7 @@
} }
.no-more { .no-more {
color: #555; color: #697777;
padding: 40px 20px; padding: 40px 20px;
text-align: center; text-align: center;
} }

Some files were not shown because too many files have changed in this diff Show More