hotfix/hotfix-catalog-filter
Ernest Litvinenko 2024-03-15 21:34:06 +03:00
commit d6a85cf349
66 changed files with 8902 additions and 0 deletions

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

40
.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
./idea/
./docker-compose.yml
./Dockerfile

40
README.md Normal file
View File

@ -0,0 +1,40 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

48
next.config.mjs Normal file
View File

@ -0,0 +1,48 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'relynolli.ru',
pathname: '/upload/**',
},
{
protocol: 'https',
hostname: 'tehnohimgrupp.ru',
pathname: '/upload/**',
},
],
},
webpack(config) {
const fileLoaderRule = config.module.rules.find((rule) =>
rule.test?.test?.('.svg'),
)
config.module.rules.push(
// Reapply the existing rule, but only for svg imports ending in ?url
{
...fileLoaderRule,
test: /\.svg$/i,
resourceQuery: /url/, // *.svg?url
},
// Convert all other *.svg imports to React components
{
test: /\.svg$/i,
issuer: fileLoaderRule.issuer,
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url
use: ['@svgr/webpack'],
},
)
// Modify the file loader rule to ignore *.svg, since we have it handled now.
fileLoaderRule.exclude = /\.svg$/i
return config
}
};
export default nextConfig;

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "relynolly",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@nextui-org/react": "^2.2.9",
"@tanstack/react-query": "^5.20.5",
"@tanstack/react-query-devtools": "^5.21.7",
"axios": "^1.6.7",
"framer-motion": "^11.0.3",
"libphonenumber-js": "^1.10.56",
"lodash": "^4.17.21",
"mysql2": "^3.9.1",
"nanoid": "^5.0.5",
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.50.1",
"react-imask": "^7.5.0",
"valtio": "^1.13.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@tanstack/eslint-plugin-query": "^5.20.1",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

BIN
public/10w40.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/5w30.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
public/achievements1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 KiB

BIN
public/achievements2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

BIN
public/achievements3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
public/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -0,0 +1,10 @@
<svg width="12" height="21" viewBox="0 0 12 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="banner_arr_btn.svg" clip-path="url(#clip0_1_55)">
<path id="Vector" d="M2.04428 2L9.33 10.5M2.04428 19L5.68714 14.75L6.59786 13.6875" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_1_55">
<rect width="11" height="21" fill="white" transform="translate(0.329987)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 421 B

3
public/cart.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="34" height="35" viewBox="0 0 34 35" xmlns="http://www.w3.org/2000/svg">
<path d="M3.16908 3.74161C3.03618 3.69537 2.89542 3.676 2.75497 3.68461C2.61452 3.69322 2.47718 3.72964 2.35092 3.79176C2.22466 3.85388 2.112 3.94046 2.01947 4.04648C1.92694 4.1525 1.8564 4.27584 1.81193 4.40934C1.76746 4.54284 1.74995 4.68385 1.76042 4.82417C1.77089 4.96449 1.80913 5.10134 1.87292 5.22677C1.93671 5.35219 2.02477 5.4637 2.13201 5.55481C2.23924 5.64592 2.3635 5.71483 2.49758 5.75752L2.873 5.88361C3.83067 6.20236 4.46533 6.41627 4.93142 6.63302C5.37342 6.83844 5.56467 7.00419 5.6865 7.17419C5.80975 7.34419 5.90608 7.57652 5.96133 8.06102C6.01942 8.57244 6.02083 9.24111 6.02083 10.2512V14.0365C6.02083 15.9731 6.02083 17.5357 6.18658 18.7639C6.35658 20.0389 6.72492 21.1128 7.57775 21.9656C8.42917 22.8184 9.50442 23.1839 10.7794 23.3554C12.0062 23.5211 13.5688 23.5211 15.5054 23.5211H25.5C25.7818 23.5211 26.052 23.4092 26.2513 23.2099C26.4506 23.0107 26.5625 22.7404 26.5625 22.4586C26.5625 22.1768 26.4506 21.9066 26.2513 21.7073C26.052 21.508 25.7818 21.3961 25.5 21.3961H15.5833C13.5504 21.3961 12.1323 21.3933 11.0613 21.2502C10.0229 21.1099 9.47183 20.8535 9.07942 20.4625C8.7465 20.1296 8.51275 19.6819 8.36258 18.9169H22.6993C24.0578 18.9169 24.7364 18.9169 25.2691 18.5656C25.8017 18.2143 26.0695 17.5909 26.605 16.3414L27.2113 14.9248C28.3588 12.2473 28.9326 10.9099 28.3022 9.95511C27.6717 9.00027 26.2168 9.00027 23.3042 9.00027H8.13875C8.13482 8.6061 8.1126 8.21231 8.07217 7.82019C7.99425 7.13311 7.82283 6.50269 7.41058 5.93177C6.99833 5.35944 6.45433 4.99677 5.82817 4.70636C5.23742 4.43152 4.488 4.18219 3.60117 3.88469L3.16908 3.74161ZM10.625 26.0003C11.1886 26.0003 11.7291 26.2242 12.1276 26.6227C12.5261 27.0212 12.75 27.5617 12.75 28.1253C12.75 28.6889 12.5261 29.2294 12.1276 29.6279C11.7291 30.0264 11.1886 30.2503 10.625 30.2503C10.0614 30.2503 9.52091 30.0264 9.1224 29.6279C8.72388 29.2294 8.5 28.6889 8.5 28.1253C8.5 27.5617 8.72388 27.0212 9.1224 26.6227C9.52091 26.2242 10.0614 26.0003 10.625 26.0003ZM23.375 26.0003C23.9386 26.0003 24.4791 26.2242 24.8776 26.6227C25.2761 27.0212 25.5 27.5617 25.5 28.1253C25.5 28.6889 25.2761 29.2294 24.8776 29.6279C24.4791 30.0264 23.9386 30.2503 23.375 30.2503C22.8114 30.2503 22.2709 30.0264 21.8724 29.6279C21.4739 29.2294 21.25 28.6889 21.25 28.1253C21.25 27.5617 21.4739 27.0212 21.8724 26.6227C22.2709 26.2242 22.8114 26.0003 23.375 26.0003Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

3
public/cross.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path d="M14 1.9999L11.9999 0L7 5.0001L1.9999 0L0 1.9999L4.99987 7L0 12.0001L1.9999 14L7 8.9999L11.9999 14L14 12.0001L8.99967 7L14 1.9999Z"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

3
public/delivery.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="92" height="84" viewBox="0 0 92 84" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.166504 4.50016C0.166504 3.39509 0.605491 2.33529 1.38689 1.55388C2.16829 0.772483 3.2281 0.333496 4.33317 0.333496H50.1665C51.2716 0.333496 52.3314 0.772483 53.1128 1.55388C53.8942 2.33529 54.3332 3.39509 54.3332 4.50016V25.3335H70.9998C73.7357 25.3335 76.4448 25.8724 78.9724 26.9193C81.5 27.9663 83.7967 29.5009 85.7312 31.4354C87.6658 33.37 89.2004 35.6666 90.2473 38.1943C91.2943 40.7219 91.8332 43.431 91.8332 46.1668V62.8335C91.8336 65.5158 90.9715 68.1271 89.3741 70.2819C87.7768 72.4367 85.5289 74.0208 82.9623 74.8002C82.1982 77.321 80.6572 79.536 78.5593 81.129C76.4615 82.722 73.914 83.6115 71.2805 83.6707C68.6471 83.7298 66.0623 82.9555 63.895 81.4583C61.7278 79.9611 60.0889 77.8175 59.2123 75.3335H32.7915C31.9149 77.8175 30.2761 79.9611 28.1088 81.4583C25.9416 82.9555 23.3567 83.7298 20.7233 83.6707C18.0898 83.6115 15.5424 82.722 13.4445 81.129C11.3467 79.536 9.80568 77.321 9.0415 74.8002C6.47417 74.0215 4.22534 72.4378 2.6272 70.2829C1.02905 68.128 0.166345 65.5163 0.166504 62.8335V46.1668H25.1665C26.2716 46.1668 27.3314 45.7278 28.1128 44.9464C28.8942 44.165 29.3332 43.1052 29.3332 42.0002C29.3332 40.8951 28.8942 39.8353 28.1128 39.0539C27.3314 38.2725 26.2716 37.8335 25.1665 37.8335H0.166504V29.5002H16.8332C17.9382 29.5002 18.998 29.0612 19.7794 28.2798C20.5608 27.4984 20.9998 26.4386 20.9998 25.3335C20.9998 24.2284 20.5608 23.1686 19.7794 22.3872C18.998 21.6058 17.9382 21.1668 16.8332 21.1668H0.166504V4.50016ZM54.3332 67.0002H59.2123C60.0361 64.6687 61.5328 62.6339 63.5131 61.1532C65.4934 59.6725 67.8685 58.8124 70.3377 58.6817C72.807 58.551 75.2595 59.1556 77.3851 60.419C79.5107 61.6824 81.2138 63.5478 82.279 65.7793C82.6665 65.3929 82.9738 64.9338 83.1833 64.4282C83.3928 63.9227 83.5004 63.3807 83.4998 62.8335V46.1668C83.4998 42.8516 82.1829 39.6722 79.8387 37.328C77.4945 34.9838 74.315 33.6668 70.9998 33.6668H54.3332V67.0002ZM25.1665 71.1668C25.1665 70.0618 24.7275 69.0019 23.9461 68.2205C23.1647 67.4391 22.1049 67.0002 20.9998 67.0002C19.8948 67.0002 18.835 67.4391 18.0536 68.2205C17.2722 69.0019 16.8332 70.0618 16.8332 71.1668C16.8332 72.2719 17.2722 73.3317 18.0536 74.1131C18.835 74.8945 19.8948 75.3335 20.9998 75.3335C22.1049 75.3335 23.1647 74.8945 23.9461 74.1131C24.7275 73.3317 25.1665 72.2719 25.1665 71.1668ZM68.054 68.221C67.6659 68.607 67.3583 69.0661 67.1487 69.5717C66.9392 70.0773 66.8319 70.6195 66.8332 71.1668C66.833 72.1308 67.167 73.0651 67.7784 73.8104C68.3898 74.5557 69.2407 75.0659 70.1862 75.2542C71.1316 75.4424 72.113 75.297 72.9633 74.8428C73.8135 74.3885 74.48 73.6535 74.849 72.7629C75.2181 71.8724 75.267 70.8814 74.9873 69.9589C74.7076 69.0364 74.1167 68.2393 73.3153 67.7036C72.5138 67.1679 71.5515 66.9267 70.5921 67.021C69.6327 67.1154 68.7357 67.5394 68.054 68.221Z" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,3 @@
<svg width="29" height="28" viewBox="0 0 29 28" xmlns="http://www.w3.org/2000/svg">
<path d="M25 24.4959C25 25.458 23.901 26.0066 23.132 25.4283L15.2013 19.4632C14.7859 19.1508 14.214 19.1508 13.7987 19.4632L5.86794 25.4283C5.09903 26.0066 4 25.458 4 24.4959V3.14087C4 2.61723 4.27656 2.11504 4.76884 1.74479C5.26113 1.37452 5.9288 1.1665 6.625 1.1665H22.375C23.0711 1.1665 23.7388 1.37452 24.2312 1.74479C24.7234 2.11504 25 2.61723 25 3.14087V24.4959Z" />
</svg>

After

Width:  |  Height:  |  Size: 464 B

36
public/header_logo.svg Normal file
View File

@ -0,0 +1,36 @@
<svg width="271" height="34" viewBox="0 0 271 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_286)">
<g clip-path="url(#clip1_1_286)">
<g clip-path="url(#clip2_1_286)">
<mask id="mask0_1_286" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="271" height="34">
<path d="M270.161 0.0700073H0.426086V33.0935H270.161V0.0700073Z" fill="white"/>
</mask>
<g mask="url(#mask0_1_286)">
<path d="M106.468 16.5817H118.388L122.272 22.8857L131.823 16.5618L136.065 16.5817L121.914 25.8932L118.697 33.0935H109.186L112.313 26.0127L106.468 16.5817Z" fill="white"/>
<path d="M138.505 16.5817L131.136 33.0935H135.358L138.455 26.1123L155.824 33.0935H160.654L168.003 16.5817H163.83L160.664 23.6824L143.166 16.5817H138.505Z" fill="white"/>
<path d="M183.668 33.0935H171.3C169.836 33.0935 168.68 32.7349 167.834 32.008C166.918 31.2212 166.46 30.1158 166.46 28.6817C166.46 27.5265 166.768 26.2418 167.396 24.8276C168.402 22.5371 169.895 20.6151 171.877 19.0714C174.028 17.3984 176.279 16.5718 178.639 16.5718H191.008C192.482 16.5718 193.647 16.9203 194.494 17.6274C195.41 18.3843 195.868 19.4698 195.868 20.9039C195.868 22.079 195.549 23.3836 194.902 24.8276C193.906 27.0883 192.402 29.0004 190.401 30.554C188.23 32.237 185.989 33.0835 183.658 33.0835L183.668 33.0935ZM186.198 20.6748C186.198 19.8383 185.6 19.42 184.415 19.42H182.294C181.199 19.42 180.302 19.8084 179.595 20.5951C179.147 21.0931 178.649 21.9595 178.101 23.1944L176.926 25.8136C176.289 27.2377 175.96 28.2734 175.96 28.9207C175.96 29.8071 176.468 30.2452 177.474 30.2452H179.595C180.721 30.2452 181.697 29.7573 182.533 28.7813C183.081 28.1439 183.649 27.148 184.246 25.8235L185.421 23.2044C185.939 22.0292 186.188 21.1927 186.188 20.6748H186.198Z" fill="white"/>
<path d="M97.5445 30.2552C96.3395 30.2552 95.742 29.8469 95.742 29.0303C95.742 28.5124 96.0109 27.6659 96.5387 26.4808L100.901 16.5917H90.892L83.5524 33.1034H106.069L107.344 30.2652H97.5445V30.2552Z" fill="white"/>
<path d="M208.147 30.2552C206.942 30.2552 206.345 29.8469 206.345 29.0303C206.345 28.5124 206.614 27.6659 207.141 26.4808L211.503 16.5917H201.495L194.155 33.1034H216.672L217.947 30.2652H208.147V30.2552Z" fill="white"/>
<path d="M233.881 30.2552C232.676 30.2552 232.078 29.8469 232.078 29.0303C232.078 28.5124 232.347 27.6659 232.875 26.4808L237.237 16.5917H227.228L219.889 33.1034H242.406L243.68 30.2652H233.881V30.2552Z" fill="white"/>
<path d="M245.483 33.0935H255.491L262.831 16.5817H252.753L245.483 33.0935Z" fill="white"/>
<path d="M25.1837 0.0700073H15.1054L0.426086 33.0935H10.4646L25.1837 0.0700073Z" fill="white"/>
<path d="M266.347 8.66447L270.161 0.0700073H260.023L259.226 1.88251C258.19 4.2328 255.87 5.74654 253.301 5.74654H95.6922L94.4175 8.66447H266.347Z" fill="white"/>
<path d="M38.0406 33.0935L27.056 16.751L29.2071 12.2496H36.736C38.0207 12.2496 39.2556 11.9808 40.4407 11.433C41.7951 10.8255 42.6914 10.0089 43.1594 8.9732C43.3287 8.5848 43.4084 8.23624 43.4084 7.91756C43.4084 6.47353 42.1735 5.74654 39.7037 5.74654H28.8187L16.3403 33.0935H11.9086L26.9564 0.0700073H50.8476C52.9489 0.0700073 54.6818 0.46836 56.0262 1.27503C57.6694 2.24103 58.496 3.68506 58.496 5.60711C58.496 6.66275 58.247 7.70843 57.7591 8.73418C56.6038 11.204 54.6519 13.2754 51.8833 14.9485C49.4136 16.3925 46.655 17.3585 43.5876 17.8365C50.3696 27.4369 54.2336 33.0935 54.2336 33.0935H38.0406Z" fill="#B3C53F"/>
<path d="M75.7547 19.42H86.3409L87.6455 16.5817H64.541L57.0121 33.0935H80.1067L81.4113 30.2552H71.3628C70.1279 30.2552 69.5105 29.8469 69.5105 29.0303C69.5105 28.5124 69.7794 27.6659 70.3271 26.4808L72.6077 21.4217C73.1554 20.2068 74.3903 19.43 75.7447 19.43L75.7547 19.42Z" fill="#B3C53F"/>
<path d="M79.0909 26.2617L80.3955 23.4135H74.3704H74.1413L72.8467 26.2617H79.0909Z" fill="#B3C53F"/>
</g>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_1_286">
<rect width="270" height="33.86" fill="white" transform="translate(0.368164 0.0700073)"/>
</clipPath>
<clipPath id="clip1_1_286">
<rect width="270" height="33.86" fill="white" transform="translate(0.368164 0.0700073)"/>
</clipPath>
<clipPath id="clip2_1_286">
<rect width="269.884" height="33.86" fill="white" transform="translate(0.426086 0.0700073)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

11
public/header_reg.svg Normal file
View File

@ -0,0 +1,11 @@
<svg width="17" height="20" viewBox="0 0 17 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_338)">
<path d="M8.6286 13.1746C4.3156 13.1746 0.631592 13.8546 0.631592 16.5746C0.631592 19.2956 4.2926 19.9996 8.6286 19.9996C12.9416 19.9996 16.6256 19.3206 16.6256 16.5996C16.6256 13.8786 12.9656 13.1746 8.6286 13.1746Z" />
<path opacity="0.4" d="M8.62855 10.5837C11.5665 10.5837 13.9205 8.22871 13.9205 5.29171C13.9205 2.35471 11.5665 -0.000289917 8.62855 -0.000289917C5.69155 -0.000289917 3.33655 2.35471 3.33655 5.29171C3.33655 8.22871 5.69155 10.5837 8.62855 10.5837Z"/>
</g>
<defs>
<clipPath id="clip0_1_338">
<rect width="16" height="20" fill="white" transform="translate(0.631836)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 745 B

4
public/header_search.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 20C15.7467 20 20 15.7467 20 10.5C20 5.25329 15.7467 1 10.5 1C5.25329 1 1 5.25329 1 10.5C1 15.7467 5.25329 20 10.5 20Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 18L25 25" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 393 B

BIN
public/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

15
public/home_icon.svg Normal file
View File

@ -0,0 +1,15 @@
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_94_253)">
<mask id="mask0_94_253" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="15" height="16">
<path d="M15 0.5H0V15.5H15V0.5Z" fill="white"/>
</mask>
<g mask="url(#mask0_94_253)">
<path d="M13.6332 9.02975C13.7725 9.02975 13.906 9.08506 14.0045 9.18352C14.1029 9.28197 14.1582 9.41551 14.1582 9.55475V13.9603C14.1785 14.5693 14.105 14.969 13.8327 15.2203C13.5792 15.4535 13.2117 15.5203 12.7355 15.4948H2.35774C1.87474 15.4707 1.49974 15.275 1.32574 14.8798C1.21024 14.6195 1.15624 14.312 1.15624 13.9587V9.554C1.15624 9.41476 1.21155 9.28123 1.31001 9.18277C1.40846 9.08431 1.542 9.029 1.68124 9.029C1.82048 9.029 1.95401 9.08431 2.05247 9.18277C2.15092 9.28123 2.20624 9.41476 2.20624 9.554V13.9587C2.20624 14.1327 2.22574 14.2738 2.25874 14.3802L2.28574 14.4537L2.28349 14.4462C2.28424 14.4365 2.30599 14.4403 2.38249 14.4447H12.7625C12.9462 14.4553 13.0632 14.4447 13.1045 14.444L13.1075 14.4432C13.0977 14.4035 13.1165 14.2408 13.1075 13.9767V9.554C13.1075 9.48499 13.1211 9.41666 13.1475 9.35292C13.174 9.28917 13.2127 9.23126 13.2615 9.1825C13.3104 9.13374 13.3683 9.09509 13.4321 9.06875C13.4959 9.04241 13.5642 9.02965 13.6332 9.02975ZM7.82524 0.5C8.02324 0.5 8.20024 0.578 8.36674 0.72275L14.8355 6.827C14.8865 6.87414 14.9277 6.93093 14.9566 6.99409C14.9855 7.05725 15.0016 7.12552 15.0039 7.19494C15.0062 7.26436 14.9948 7.33355 14.9702 7.39851C14.9456 7.46347 14.9083 7.5229 14.8606 7.57336C14.8129 7.62382 14.7556 7.66431 14.6921 7.69248C14.6286 7.72065 14.5602 7.73594 14.4907 7.73746C14.4213 7.73898 14.3522 7.72671 14.2875 7.70136C14.2229 7.676 14.1639 7.63807 14.114 7.58975L7.80124 1.63175L0.868237 7.6055C0.762813 7.6965 0.625557 7.7419 0.486663 7.7317C0.347769 7.7215 0.218614 7.65655 0.127612 7.55112C0.0366092 7.4457 -0.00878664 7.30844 0.00141065 7.16955C0.0116079 7.03066 0.076563 6.9015 0.181987 6.8105L7.26649 0.70775L7.33099 0.65975C7.49149 0.55925 7.65199 0.50075 7.82599 0.50075L7.82524 0.5Z" fill="#8F8F8F"/>
</g>
</g>
<defs>
<clipPath id="clip0_94_253">
<rect width="15" height="15" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

3
public/minus_icon.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="12" height="2" viewBox="0 0 12 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 0H11.25C11.4489 0 11.6397 0.079018 11.7803 0.21967C11.921 0.360323 12 0.551088 12 0.75C12 0.948912 11.921 1.13968 11.7803 1.28033C11.6397 1.42098 11.4489 1.5 11.25 1.5H6.75H0.75C0.551088 1.5 0.360322 1.42098 0.21967 1.28033C0.0790178 1.13968 0 0.948912 0 0.75C0 0.551088 0.0790178 0.360323 0.21967 0.21967C0.360322 0.079018 0.551088 0 0.75 0H6.75Z" fill="#151515"/>
</svg>

After

Width:  |  Height:  |  Size: 482 B

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/oil1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 MiB

BIN
public/oil2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 KiB

BIN
public/oil3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
public/oil4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
public/oil4.zip Normal file

Binary file not shown.

BIN
public/oiltypeImage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

3
public/plus_icon.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 0C6.19891 0 6.38968 0.0790178 6.53033 0.21967C6.67098 0.360322 6.75 0.551088 6.75 0.75V5.25H11.25C11.4489 5.25 11.6397 5.32902 11.7803 5.46967C11.921 5.61032 12 5.80109 12 6C12 6.19891 11.921 6.38968 11.7803 6.53033C11.6397 6.67098 11.4489 6.75 11.25 6.75H6.75V11.25C6.75 11.4489 6.67098 11.6397 6.53033 11.7803C6.38968 11.921 6.19891 12 6 12C5.80109 12 5.61032 11.921 5.46967 11.7803C5.32902 11.6397 5.25 11.4489 5.25 11.25V6.75H0.75C0.551088 6.75 0.360322 6.67098 0.21967 6.53033C0.0790178 6.38968 0 6.19891 0 6C0 5.80109 0.0790178 5.61032 0.21967 5.46967C0.360322 5.32902 0.551088 5.25 0.75 5.25H5.25V0.75C5.25 0.551088 5.32902 0.360322 5.46967 0.21967C5.61032 0.0790178 5.80109 0 6 0Z" fill="#151515"/>
</svg>

After

Width:  |  Height:  |  Size: 822 B

11
public/storage.svg Normal file
View File

@ -0,0 +1,11 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_456_6002)">
<path d="M100 25L49.625 0L0 25V31.25H6.25V100H18.75V43.75H81.25V100H93.75V31.25H100V25ZM25 37.5V31.25H37.5V37.5H25ZM43.75 37.5V31.25H56.25V37.5H43.75ZM62.5 37.5V31.25H75V37.5H62.5Z" />
<path d="M37.5 56.25H31.25V50H25V68.75H43.75V50H37.5V56.25ZM37.5 81.25H31.25V75H25V93.75H43.75V75H37.5V81.25ZM62.5 81.25H56.25V75H50V93.75H68.75V75H62.5V81.25Z" />
</g>
<defs>
<clipPath id="clip0_456_6002">
<rect width="100" height="100" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 578 B

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@ -0,0 +1,64 @@
import {Input} from "@nextui-org/input";
import {Button, Checkbox} from "@nextui-org/react";
import {ChevronRightIcon} from "@nextui-org/shared-icons";
import {useQuery} from "@tanstack/react-query";
import LocalAPI from "@/service/localAPI";
type OrderInfoProps = {
setIsDisabled: (value: boolean) => void
isDisabled: boolean
}
const OrderInfo = (props: OrderInfoProps) => {
const totalProductPriceQs = useQuery(
{
queryKey: ['totalProductPrice'], queryFn: async () => {
const service = new LocalAPI()
return await service.totalProductPrice()
}
}
)
return (
<div className="confirm shadow-lg p-7 text-[#333333] mb-4">
<h2 className={"text-subtitle-2 font-bold mb-2"}>Информация о заказе</h2>
<div className="flex justify-between mb-2">
<span>Товаров на:</span>
<span>{String(totalProductPriceQs.data?.total_product_price).replace(/\B(?=(\d{3})+(?!\d))/g, " ")} </span>
</div>
<form className="flex justify-between h-[50px] mb-2">
<Input type="text" label="Введите промокод" variant={"bordered"}
className={"border-[#1E1E1E] mr-4"}/>
<Button className={"flex-auto h-full"}
color={'primary'}><ChevronRightIcon/></Button>
</form>
{/*TODO calculate discount*/}
<div className="flex justify-between mb-2 border-b-1 border-b-black-1 pb-4">
<span>Скидка: </span>
<span>0 </span>
</div>
{/*TODO calculate discount + shipment*/}
<div className="flex justify-between mb-2 items-center">
<span>Итого: </span>
<span className={"text-title-3 font-bold"}>{String(totalProductPriceQs.data?.total_product_price).replace(/\B(?=(\d{3})+(?!\d))/g, " ")} </span>
</div>
<div className="flex justify-between mb-8 text-[#808080] text-subtitle-5">
<span>Сумма НДС: </span>
<span>{totalProductPriceQs.data?.total_product_price * 0.2} </span>
</div>
<Checkbox isSelected={!props.isDisabled} onChange={() => {
props.setIsDisabled(!props.isDisabled)
}}
className={"[&_span:last-child]:!text-[#808080] [&_span:last-child]:text-subtitle-5 [&_span:last-child]:leading-normal mb-6"}>Нажимая
кнопку «Оформить заказ», я даю согласие на обработку моих персональных данных</Checkbox>
</div>
)
}
export default OrderInfo

View File

@ -0,0 +1,9 @@
const Breadcrumbs = () => {
return (
<>
</>
)
}
export default Breadcrumbs;

View File

@ -0,0 +1,281 @@
import {
Dropdown, DropdownItem,
DropdownMenu,
DropdownTrigger,
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem
} from "@nextui-org/react";
import Link from "next/link";
import Logo from "@/../public/header_logo.svg";
import SearchIcon from "@/../public/header_search.svg";
import RegistrationIcon from "@/../public/header_reg.svg";
import CartLogo from "@/../public/cart.svg"
import {Badge} from "@nextui-org/badge";
import {useQuery, useQueryClient} from "@tanstack/react-query";
import LocalAPI from "@/service/localAPI";
import {HTMLProps, useEffect, useState} from "react";
import {tv} from "tailwind-variants"
const NavbarMenuToggle = ({isOpened, ...props}: { isOpened: boolean} & HTMLProps<HTMLDivElement>) => {
const wrapperClass = tv({
base: "border-1 border-[#404040] rounded-[8px] group hover:border-green-2 transition-colors cursor-pointer px-3 h-[50px] w-[50px] items-center justify-center flex xl:hidden",
})
const spanClass = tv({
base: "absolute w-full h-[2px] left-0 stroke-white bg-white content-[' '] transition-all",
variants: {
position: {
top: "top-0",
center: "top-[10px]",
bottom: "top-[20px]",
},
isOpened: {
true: "",
false: ""
}
},
compoundVariants: [
{
position: "top",
isOpened: true,
class: "origin-left rotate-45 w-[32px] translate-y-[-1px]",
},
{
position: "center",
isOpened: true,
class: "origin-left rotate-45 w-[31px] top-0 opacity-0"
},
{
position: "bottom",
isOpened: true,
class: "origin-left -rotate-45 translate-y-[1px] w-[31px] bottom-0",
}
]
})
return (
<div className={wrapperClass()} {...props}>
<div className="relative w-[22px] h-[22px]">
<span className={spanClass({position: "top", isOpened: isOpened})}></span>
<span className={spanClass({position: "center", isOpened: isOpened})}></span>
<span className={spanClass({position: "bottom", isOpened: isOpened})}></span>
</div>
</div>
)
}
const Header = () => {
const menuItems = [
{title: "Каталог", href: "/catalog"},
{title: "Бренд", href: "#"},
{title: "Статьи", href: "#"},
{title: "Контакты", href: "#"},
]
const qs = useQueryClient()
const cart = useQuery({
queryKey: ['cart'], queryFn: async () => {
const service = new LocalAPI()
return await service.getCartItems()
}
})
const [isOpened, setIsOpened] = useState(false);
return (
<header className={"w-full top-0 left-0 bg-black-4 py-[33px] z-50 h-[100px] relative"}>
<div className="wrapper flex justify-between items-center ">
<Link href={"/"} className={"transition-none hover:opacity-100 w-1/4"}>
<Logo/>
</Link>
<nav className={"xl:flex justify-between items-center w-[30%] hidden"}>
{
menuItems.map(item => (
<Link href={item.href}
className={"text-header-link text-white hover:text-green-2 hover:opacity-100 transition-colors"}
key={item.title}>{item.title}</Link>
))
}
</nav>
<div className="w-[35%] xl:w-[30%] xl:flex justify-end [&>*]:mr-4 items-center hidden">
<div
className={'border-1 border-[#404040] rounded-[8px] group hover:border-green-2 transition-colors cursor-pointer px-3 h-[50px] flex items-center'}>
<SearchIcon className={"stroke-2 stroke-white group-hover:stroke-green-2 transition-colors"}/>
</div>
<div
className={"border-1 border-[#404040] rounded-[8px] group hover:border-green-2 transition-colors cursor-pointer px-3 h-[50px] flex items-center"}>
<Link href={"#"}
className={"text-xs 2xl:text-header-link text-white group-hover:text-green-2 group-hover:opacity-100 transition-colors"}>+7(495)191-97-20</Link>
</div>
<div className={"flex flex-row items-center group"}>
<Dropdown>
<DropdownTrigger>
<button
className={"flex flex-row items-center border-[#404040] border-1 hover:border-green-2 rounded-[8px] transition-colors h-[50px] px-3"}>
<RegistrationIcon
className={"mr-2 fill-white group-hover:fill-green-2 transition-colors"}/>
<span
className={"text-white group-hover:text-green-2 transition-colors"}>Вход</span>
</button>
</DropdownTrigger>
<DropdownMenu className={"text-black"}>
<DropdownItem key={"business-login"}>Войти как Юр. Лицо</DropdownItem>
<DropdownItem key={"personal-login"}>Войти как Физ. Лицо</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
{cart.data &&
<Badge isInvisible={cart.data?.length === 0} color={"primary"} content={cart.data?.length}
className={"text-black font-semibold"}>
<Link href={'/cart'}
className={"rounded-[8px] group transition-colors cursor-pointer bg-transparent hover:bg-primary flex items-center px-3 h-[50px]"}>
<CartLogo className={"fill-white group-hover:fill-black-1 transition-colors"}/>
</Link>
</Badge>
}
</div>
<NavbarMenuToggle isOpened={isOpened} onClick={() => setIsOpened(!isOpened)}/>
</div>
<div className={tv({
base: "bg-black-4 absolute transition-all -z-10 left-0 w-full",
variants: {
open: {
false: "top-[-500px] opacity-0",
true: "top-[100px] opacity-100",
},
}
})({open: isOpened})}>
<div className="wrapper py-10 flex justify-between flex-col md:flex-row xl:hidden relative">
<nav className={"flex flex-col justify-between items-start w-[full] pb-10"}>
{
menuItems.map(item => (
<Link href={item.href}
className={"text-2xl text-white hover:text-green-2 hover:opacity-100 transition-colors"}
key={item.title}>{item.title}</Link>
))
}
</nav>
<div className="flex flex-row flex-wrap justify-stretch [&>*]:mr-4 items-start">
<div
className={"border-1 border-[#404040] rounded-[8px] group hover:border-green-2 transition-colors cursor-pointer px-3 h-[50px] flex items-center w-full mb-4"}>
<Link href={"#"}
className={"text-xs 2xl:text-header-link text-white group-hover:text-green-2 group-hover:opacity-100 transition-colors"}>+7(495)191-97-20</Link>
</div>
<div className={"flex flex-row items-center group w-1/2"}>
<Dropdown>
<DropdownTrigger>
<button
className={"flex flex-row items-center border-[#404040] border-1 hover:border-green-2 rounded-[8px] transition-colors h-[50px] px-3"}>
<RegistrationIcon
className={"mr-2 fill-white group-hover:fill-green-2 transition-colors"}/>
<span
className={"text-white group-hover:text-green-2 transition-colors"}>Вход</span>
</button>
</DropdownTrigger>
<DropdownMenu className={"text-black"}>
<DropdownItem key={"business-login"}>Войти как Юр. Лицо</DropdownItem>
<DropdownItem key={"personal-login"}>Войти как Физ. Лицо</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
{cart.data &&
<Badge isInvisible={cart.data?.length === 0} color={"primary"} content={cart.data?.length}
className={"text-black font-semibold"}>
<Link href={'/cart'}
className={"rounded-[8px] group transition-colors cursor-pointer bg-transparent hover:bg-primary flex items-center px-3 h-[50px]"}>
<CartLogo className={"fill-white group-hover:fill-black-1 transition-colors"}/>
</Link>
</Badge>
}
</div>
</div>
</div>
</header>
)
}
const OldHeader = () => {
const menuItems = [
"Корзина",
"Блог",
"Производство",
"Контакты",
"Где купить"
]
return (
<Navbar className={"bg-black 2xl:max-w-[1440px"}>
<NavbarContent justify={"start"}>
<NavbarBrand className={"mr-4"}>
</NavbarBrand>
</NavbarContent>
<NavbarContent justify={"center"} className={"gap-14"}>
{
menuItems.map(item => (
<NavbarItem key={item}>
<Link href={"#"}
className={"text-header-link text-white hover:text-green-2 hover:opacity-100 transition-colors"}>{item}</Link>
</NavbarItem>
))
}
</NavbarContent>
<NavbarContent justify={'end'}>
<NavbarItem
className={'border-1 border-[#404040] rounded-[8px] group hover:border-green-2 transition-colors cursor-pointer p-3'}>
<SearchIcon className={"stroke-2 stroke-white group-hover:stroke-green-2 transition-colors"}/>
</NavbarItem>
<NavbarItem
className={"border-1 border-[#404040] rounded-[8px] group hover:border-green-2 transition-colors cursor-pointer p-3"}>
<Link href={"#"}
className={"text-header-link text-white group-hover:text-green-2 group-hover:opacity-100 transition-colors"}>+7(495)191-97-20</Link>
</NavbarItem>
<NavbarItem className={"flex flex-row items-center group"}>
<Dropdown>
<DropdownTrigger>
<button
className={"flex flex-row border-[#404040] border-1 hover:border-green-2 rounded-[8px] transition-colors p-3"}>
<RegistrationIcon
className={"mr-2 fill-white group-hover:fill-green-2 transition-colors"}/>
<span className={"text-white group-hover:text-green-2 transition-colors"}>Вход</span>
</button>
</DropdownTrigger>
<DropdownMenu className={"text-black"}>
<DropdownItem key={"business-login"}>Войти как Юр. Лицо</DropdownItem>
<DropdownItem key={"personal-login"}>Войти как Физ. Лицо</DropdownItem>
</DropdownMenu>
</Dropdown>
</NavbarItem>
</NavbarContent>
</Navbar>
)
}
export default Header;

View File

@ -0,0 +1,17 @@
import Header from "@/components/reusable/header";
import {Mulish} from "next/font/google";
const mulish = Mulish({
subsets: ["cyrillic", "latin"],
variable: "--font-mulish"
})
const Layout = ({ children }: { children: React.ReactNode }) => {
return ( <>
<Header />
<main className={`${mulish.variable} font-mulish`}>
{children}
</main>
</>)
}
export default Layout

View File

@ -0,0 +1,48 @@
import {ReactNode} from "react";
import {BreadcrumbItem, Breadcrumbs} from "@nextui-org/react";
import HomeIcon from "../../../public/home_icon.svg";
import Link from "next/link";
type WrapperProps = {
children: ReactNode,
title: string,
breadcrumbs?: {
name: string,
link: string,
icon?: ReactNode
}[]
}
const Wrapper = (props: WrapperProps) => {
return <>
<section className={"bg-white text-black pt-7"}>
<div className="wrapper">
{
props.breadcrumbs &&
<Breadcrumbs separator="/"
itemClasses={{
separator: "px-2"
}}
>
<BreadcrumbItem startContent={<HomeIcon/>} className={"text-white"}>
<Link href={"/"}>Главная</Link>
</BreadcrumbItem>
{
props.breadcrumbs.map(item =>
<BreadcrumbItem startContent={item.icon} key={item.name}>
<Link href={item.link}>{item.name}</Link>
</BreadcrumbItem>
)
}
</Breadcrumbs>
}
<h1 className={"mt-4 text-title-1 font-bold italic uppercase pb-16"}>{props.title}</h1>
{props.children}
</div>
</section>
</>
}
export default Wrapper;

14
src/hooks/useClient.ts Normal file
View File

@ -0,0 +1,14 @@
import {useEffect, useState} from "react";
const useClient = () => {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
return isClient
}
export default useClient;

View File

@ -0,0 +1,11 @@
import useClient from "@/hooks/useClient";
import {useEffect} from "react";
import {getCartItems} from "@/store/cart";
const useStateManager = () => {
useEffect(() => {
getCartItems()
}, [])
}
export default useStateManager;

26
src/pages/_app.tsx Normal file
View File

@ -0,0 +1,26 @@
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import {NextUIProvider} from "@nextui-org/react";
import {useRouter} from "next/navigation";
import Layout from "@/components/reusable/layout";
// import useStateManager from "@/hooks/useStateManager";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {ReactQueryDevtools} from "@tanstack/react-query-devtools";
const queryClient = new QueryClient();
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter()
return(
<NextUIProvider navigate={router.push}>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={true}/>
<Layout>
<Component {...pageProps} />
</Layout>
</QueryClientProvider>
</NextUIProvider>
);
}

13
src/pages/_document.tsx Normal file
View File

@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

13
src/pages/api/hello.ts Normal file
View File

@ -0,0 +1,13 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
type Data = {
name: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
res.status(200).json({ name: "John Doe" });
}

View File

@ -0,0 +1,51 @@
import type {NextApiRequest, NextApiResponse} from "next";
import Db from "@/service/db";
type Properties = {
acea: string,
use_areas: string,
acid_index: string,
pour_point: string,
flash_point: string,
viscosity_index: string,
viscosity_kinematic: string,
documents: string[],
main_image: string[],
images: string[],
tribological_properties: string
requirements: string,
width: string,
height: string,
length: string,
volume: string,
weight: string,
mileage: string,
box_type: string,
category: string,
oil_type: string,
viscosity: string,
subcategory: string,
vendor_code: string,
api_standart: string
}
type Price = {
BASE: number,
OPTMAX: number,
OPTMIN: number,
[key: string]: number
}
export type ResponseData = {
id: number,
code: string,
name: string,
is_active: number,
properties: Partial<Properties>,
detailText: string,
price: Price
}

View File

@ -0,0 +1,14 @@
// Получить список всех с-в товара
import {NextApiRequest, NextApiResponse} from "next";
import Db from "@/service/db";
export type ResponseData = {
id: number,
code: string,
name: string,
values: {
id: number,
value: string
}[] | null
}

View File

@ -0,0 +1,13 @@
import {NextApiRequest, NextApiResponse} from "next";
import Db from "@/service/db";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const sql = `
select srv.id as id, srv.code as code, srv.name as name, srv.DESCRIPTION as description, srv.ACTIVE as active, f.SUBDIR || '/' || f.FILE_NAME as image from b_sale_delivery_srv srv left join b_file f on srv.LOGOTIP = f.ID where srv.ACTIVE = 'Y' and code is not null and srv.id = ${req.query.id};
`
const [returnedData] = await Db.query(sql)
res.status(200).json((returnedData as object[])[0] )
}
export default handler;

View File

@ -0,0 +1,16 @@
import {NextApiRequest, NextApiResponse} from "next";
import Db from "@/service/db";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const sql = `
select srv.id as id, srv.code as code, srv.name as name, srv.DESCRIPTION as description, srv.ACTIVE as active, CONCAT(f.SUBDIR, '/', f.FILE_NAME) as image from b_sale_delivery_srv srv left join b_file f on srv.LOGOTIP = f.ID where srv.ACTIVE = 'Y' and code is not null;
`
const [returnedData] = await Db.query(sql)
res.status(200).json(returnedData)
}
export default handler;

View File

@ -0,0 +1,86 @@
import {NextApiRequest, NextApiResponse} from "next";
import {z} from "zod";
import BitrixAPIBase from "@/service/bitrixAPI";
import YookassaAPIBase from "@/service/yookassaAPI";
import Db from "@/service/db";
const schema = z.object({
fuserId: z.number(),
phoneNumber: z.string(),
fullName: z.string(),
email: z.string().email(),
deliveryType: z.string().default("takeaway"),
address: z.string().optional()
})
type schemaType = z.infer<typeof schema>
const bitrixService = new BitrixAPIBase()
const kassaService = new YookassaAPIBase()
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const result = schema.safeParse(req.body)
if (!result.success) {
res.status(400).json({error: result.error.issues})
return
}
const {body} = req as { body: schemaType }
// Для начала создаем order в битриксе
const orderId = await bitrixService.createOrderRequest("")
await bitrixService.modifyPropertyValuesForOrder(orderId, [
{
// FIO
orderPropsId: 103,
value: body.fullName,E
},
{
// Phone
orderPropsId: 107,
value: body.phoneNumber
},
{
// Email
orderPropsId: 108,
value: body.email
}
])
// Далее добавим товары в заказ
await bitrixService.addProductsToOrder(orderId, body.fuserId)
// Получаем обновленные данные о заказе
const {price, basketItems} = await bitrixService.getOrderInfo(orderId)
// Затем создадим платеж в админке
await bitrixService.createPayment(orderId, price)
// после этого идем в юкассу и создаем платеж там
//@ts-ignore
const paymentData = await kassaService.createPayment(orderId, price, basketItems.map(elem => {
return {
description: elem.name,
quantity: +elem.quantity,
amount: elem.price
}
}) as { description: string, amount: number, quantity: number }[])
const insPaymentDataStmt = `
insert into api_youkassa_payment (payment_id, order_id, link, status)
values ('${paymentData.id}',
'${orderId}',
'${paymentData.confirmation.confirmation_url}',
'${paymentData.status}');
`
await Db.query(insPaymentDataStmt)
res.status(201).json(paymentData)
}
export default handler;

View File

@ -0,0 +1,15 @@
import {NextApiRequest, NextApiResponse} from "next";
import Db from "@/service/db";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const sql = `
select sum(t2.PRICE * t1.quantity) as total_product_price from api_cart t1 join b_catalog_price t2 on t1.price_type_id = t2.CATALOG_GROUP_ID and t1.product_id = t2.PRODUCT_ID where fuser_id = ${req.body.fuserId};
`
const [result] = await Db.query(sql) as any
res.status(200).json(result[0] as {total_product_price: string})
}
export default handler;

View File

@ -0,0 +1,69 @@
import {NextApiRequest, NextApiResponse} from "next";
import {z} from "zod";
import BitrixAPIBase from "@/service/bitrixAPI";
import Db from "@/service/db";
// import Db from "@/service/db";
const bitrixService = new BitrixAPIBase()
const events = {
'payment.canceled': 'canceled',
'payment.succeeded': 'succeeded',
}
const schema = z.object({
type: z.literal("notification"),
event: z.enum(["payment.canceled", "payment.succeeded"]),
object: z.object({
id: z.string().uuid(),
status: z.enum(["pending", "succeeded", "canceled"]),
})
})
type RequestData = z.infer<typeof schema>
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const {body} = req as {body: RequestData}
const parsed = schema.safeParse(body)
if (!parsed.success) {
res.status(400).json({error: parsed.error.issues})
return
}
const {event, object} = parsed.data
const {id, status} = object
const stmt = `
update api_youkassa_payment set status = '${status}' where payment_id = '${id}'
`
await Db.query(stmt)
const [result] = await Db.query(`select t1.order_id as order_id, t2.ID as payment_id from api_youkassa_payment t1 join b_sale_order_payment t2 on t1.order_id = t2.ORDER_ID where t1.payment_id = '${id}';`) as any
if (status === 'succeeded') {
try{
await bitrixService.approvePayment(result[0].payment_id, 8)
}
catch (e) {
console.log(e)
}
}
if (status === 'canceled') {
try {
await bitrixService.cancelOrder(result[0].order_id)
}
catch (e) {
console.log(e)
}
}
res.status(200).json({"status": "changed"})
}
export default handler;

141
src/pages/cart.tsx Normal file
View File

@ -0,0 +1,141 @@
import {BreadcrumbItem, Breadcrumbs, Button, Checkbox} from "@nextui-org/react";
import Link from "next/link";
import Image from "next/image";
import MinusIcon from "@/../public/minus_icon.svg"
import PlusIcon from "@/../public/plus_icon.svg"
import CrossIcon from "@/../public/cross.svg"
import {ChevronRightIcon} from "@nextui-org/shared-icons";
import {toggleCart} from "@/store/cart"
import {ResponseData} from "@/pages/api/v1/catalog";
import {useState} from "react";
import {useRouter} from "next/navigation";
import Wrapper from "@/components/reusable/wrapper";
import OrderInfo from "@/components/pages/cart/orderInfo";
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
import LocalAPI from "@/service/localAPI";
const CartCard = (product: Partial<ResponseData> & { quantity: number, available_quantity: number }) => {
const qc = useQueryClient()
const changeQuantity = useMutation({
mutationFn: async ({productId, quantity}: { productId: number, quantity: number }) => {
const service = new LocalAPI()
return await service.changeQuantity(productId, quantity)
},
onSuccess: () => {
qc.invalidateQueries({queryKey: ['cart']})
qc.invalidateQueries({queryKey: ["totalProductPrice"]})
}
})
return (
<div
className="card relative flex bg-gray-card rounded-[20px] w-full px-7 py-4 group hover:shadow-md transition-shadow cursor-pointer justify-between items-center text-[#151515] [&>*]:mr-7">
<Image className={"grow"} src={`https://tehnohimgrupp.ru/upload/${product.properties!.main_image![0]}`}
width={100} height={126} alt={"oil"}/>
<div className="text-block flex flex-col h-1/2 justify-between py-5 w-1/3">
<p>{product.name}</p>
<p className={"text-[#8F8F8F]"}>Артикул: {product.properties!.vendor_code}</p>
</div>
<span className={"grow"}>{`${+(product.properties!.weight!) / 1000} кг`}</span>
<span className={"grow"}>{product.properties!.volume}</span>
<div className="flex grow justify-between relative z-20">
<button onClick={() => changeQuantity.mutate({productId: product.id!, quantity: product.quantity - 1})
}
className={"bg-white flex drop-shadow w-[30px] h-[30px] rounded items-center justify-center hover:bg-primary transition-colors"}>
<MinusIcon/></button>
<span>{product.quantity}</span>
<button onClick={() => changeQuantity.mutate({productId: product.id!, quantity: product.quantity + 1})}
className={"bg-white flex drop-shadow w-[30px] h-[30px] rounded items-center justify-center hover:bg-primary transition-colors"}>
<PlusIcon/></button>
</div>
<div className="flex flex-col font-bold grow text-center">
{product.price!.BASE}
</div>
<button className={"relative z-20"} onClick={() => toggleCart(product as ResponseData)}>
<CrossIcon className={"hover:fill-red-500 fill-[#8F8F8F] transition-colors"}/>
</button>
<Link href={"/catalog/" + product.code} className={"absolute top-0 left-0 w-full h-full z-10"}></Link>
</div>
)
}
const PlaceHolder = () => {
return (
<div
className="card relative flex flex-col bg-gray-card rounded-[20px] w-full px-7 py-4 justify-between items-center text-[#151515] [&>*]:mr-7 text-subtitle-3">
<div className="flex flex-col w-1/3 text-center py-16">
<span className={"pb-7"}>В корзине пока что ничего нет, выберите необходимые товары в каталоге</span>
<Link href={'/catalog'}><Button endContent={<ChevronRightIcon/>} color={"primary"}
className={"uppercase font-bold text-subtitle-3 w-2/3 mx-auto rounded-[8px] italic h-[70px]"}>перейти
в каталог</Button></Link>
</div>
</div>
)
}
const Cart = () => {
const router = useRouter();
const [isDisabled, setIsDisabled] = useState(true)
const cart = useQuery({
queryKey: ['cart'],
queryFn: async () => {
const service = new LocalAPI()
return await service.getCartItems()
}
})
return (
<Wrapper title={"Корзина"} breadcrumbs={[{name: "Корзина", link: "/cart"}]}>
<div className="flex w-full pb-16 justify-between">
{
cart.data?.length ? (
<>
<div className="cards w-8/12 [&>*]:mb-7">
{cart.data.map(item => <CartCard key={item.id} {...item}/>)}
</div>
<div className="flex flex-col w-[30%]">
<OrderInfo
setIsDisabled={setIsDisabled} isDisabled={isDisabled}/>
<Button onClick={() => router.push('/order/make')} color={"primary"}
className={"text-subtitle-3 uppercase italic font-bold h-[70px] w-full"}
isDisabled={isDisabled}>Оформить заказ</Button>
</div>
</>) : <PlaceHolder/>
}
</div>
</Wrapper>
)
}
export default Cart;

View File

@ -0,0 +1,201 @@
import {BreadcrumbItem, Breadcrumbs, Button} from "@nextui-org/react";
import HomeIcon from "../../../public/home_icon.svg";
import Link from "next/link";
import axios from "axios";
import {ResponseData} from "@/pages/api/v1/catalog";
import {InferGetStaticPropsType} from "next";
import Cart from "@/../public/cart.svg"
import Favourites from "@/../public/favourites_icon.svg"
import favouritesStore, {toggleFavourite} from "@/store/favourites";
import {useSnapshot} from "valtio";
import useClient from "@/hooks/useClient";
import LocalAPI from "@/service/localAPI";
import {useQuery, useQueryClient, useMutation} from "@tanstack/react-query";
const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => {
const {favourites} = useSnapshot(favouritesStore)
const cartItems = useQuery<any[]>({
queryKey: ['cart']
})
const isClient = useClient()
const qs = useQueryClient()
const toggleCart = useMutation({
mutationFn: async ({productId, quantity}: { productId: number, quantity: number }) => {
const service = new LocalAPI()
if (cartItems.data!.find(({id}) => id === productId)) {
return await service.deleteCartItem(productId)
}
return await service.addCartItem(productId, quantity)
},
onSuccess: () => {
qs.invalidateQueries({queryKey: ["cart"]})
}
})
return (
<>
<section className={"bg-black-4"}>
<div className="wrapper pt-8">
<Breadcrumbs separator="/"
itemClasses={{
separator: "px-2 text-[#8F8F8F]",
item: "text-[#8F8F8F]"
}}
>
<BreadcrumbItem startContent={<HomeIcon/>}>
<Link href={"/"}>Главная</Link>
</BreadcrumbItem>
<BreadcrumbItem>
<Link href={"/catalog"}>
Каталог
</Link>
</BreadcrumbItem>
<BreadcrumbItem>{product.name}</BreadcrumbItem>
</Breadcrumbs>
</div>
<div className="wrapper mt-12 grid grid-cols-2">
{/*TODO image*/}
{product.properties.main_image && <img src={product.properties.main_image[0]} alt="#"/>}
<div className="right-block">
<span
className={"text-xl text-[#E0E3E3] mb-7 block"}>Артикул: {product.properties.vendor_code}</span>
<h1 className={"text-title-3 font-bold mb-7"}>{product.name}</h1>
<div className="block">
<span
className={"block mb-7 text-xl text-[#E0E3E3]"}>Категория: {product.properties.category}</span>
<span
className={"block mb-7 text-xl text-[#E0E3E3]"}>Вязкость: {product.properties.viscosity}</span>
<span
className={"block mb-7 text-xl text-[#E0E3E3]"}>Тип: {product.properties.oil_type}</span>
</div>
<Button isDisabled color={"warning"} className={"text-black italic text-xl py-8 mb-7"}><span
className={"font-bold"}>Скидка 10%</span> при покупке более 12 шт</Button>
<div className="flex justify-between items-center w-3/4">
<span className="font-bold text-title-4 text-white text-center">
{product.price.BASE}
<span className={"block text-base text-[#E0E3E3] font-normal"}>1 шт</span>
</span>
{isClient &&
<Button onClick={() => toggleCart.mutate({productId: product.id, quantity: 1})} color={"primary"} className={"text-black font-extrabold uppercase italic h-[70px]"}
startContent={<Cart/>}>
{cartItems.data && cartItems.data.map(({id})=> id).includes(product.id) ? "В корзине" : "Добавить в корзину"}
</Button>}
{
isClient &&
<Button onClick={() => toggleFavourite(product.id)} className={`px-5 rounded-[8px] h-[70px] ${favourites.includes(product.id) ? "bg-green-2 fill-black": "bg-[#E2E2E5] bg-opacity-30 fill-white"}`}>
<Favourites />
</Button>
}
</div>
</div>
</div>
</section>
<section className={"bg-white text-black"}>
<div className="wrapper pt-12">
<h2 className={"text-title-3 font-semibold mb-4"}>Описание</h2>
<p className={"mb-16 block"}>{product.detailText}</p>
<h2 className={"text-title-3 font-semibold mb-4"}>Характеристики</h2>
<table className={"w-full mb-7"}>
<tbody className={"[&_tr:nth-child(2n+1)]:bg-[#F7F6F8] [&_td]:p-4 [&_td]:w-1/2"}>
<tr>
<td>Объём л.</td>
<td>{product.properties.volume}</td>
</tr>
<tr>
<td>Вязкость</td>
<td>{product.properties.viscosity}</td>
</tr>
<tr>
<td>Тип моторного масла</td>
<td>{product.properties.oil_type}</td>
</tr>
<tr>
<td>Индекс вязкости</td>
<td>{product.properties.viscosity_index}</td>
</tr>
<tr>
<td>Вязкость кинематическая при 100°С, сСт</td>
<td>{product.properties.viscosity_kinematic}</td>
</tr>
<tr>
<td>Температура застывания C°</td>
<td>{product.properties.pour_point}</td>
</tr>
<tr>
<td>Температура вспышки в открытом тигле C° (не ниже)</td>
<td>{product.properties.flash_point}</td>
</tr>
<tr>
<td>Щелочное число, мг КОН на 1г масла (не менее)</td>
<td>{product.properties.acid_index}</td>
</tr>
<tr>
<td>Спецификации ACEA</td>
<td>{product.properties.acea}</td>
</tr>
<tr>
<td>Трибологические свойства на четырехшариковой машине трения: Показатель износа при
нагрузке 392Н в течении часа, мм (не более)
</td>
<td>{product.properties.tribological_properties}</td>
</tr>
<tr>
<td>Соответствие требованиям</td>
<td>{product.properties.requirements || null}</td>
</tr>
<tr>
<td>Области применения</td>
<td>{product.properties.use_areas}</td>
</tr>
</tbody>
</table>
<h2 className={"text-title-3 font-semibold mb-4"}>Документация</h2>
{/* TODO Документация */}
</div>
</section>
</>
)
}
export default OilCard
export const getStaticPaths = async () => {
const {data} = await axios.get<ResponseData[]>("http://localhost:8000/api/v1/catalog")
const codes = data.map(item => ({params: {code: item.code}}))
return {
paths: [
...codes
],
fallback: 'blocking'
}
}
export const getStaticProps = async ({params: {code}}: { params: { code: string } }) => {
const {data} = await axios.get<ResponseData>(`http://localhost:8000/api/v1/catalog/${code}`)
return {
props: {
product: data
}
}
}

148
src/pages/catalog/index.tsx Normal file
View File

@ -0,0 +1,148 @@
import {
Breadcrumbs,
BreadcrumbItem,
CheckboxGroup,
Checkbox,
Button, Skeleton
} from "@nextui-org/react";
import Image from "next/image";
import Link from "next/link";
import axios from "axios";
import {InferGetStaticPropsType} from "next";
import {ResponseData} from "@/pages/api/v1/catalog/properties"
import {ResponseData as ResponseDataProduct} from "@/pages/api/v1/catalog"
import FavouriteIcon from "@/../public/favourites_icon.svg";
import {Dispatch, SetStateAction, useEffect, useState} from "react";
import {toggleFavourite} from "@/store/favourites";
import {useSnapshot} from "valtio";
import favouritesStore from "@/store/favourites";
import useClient from "@/hooks/useClient";
import Wrapper from "@/components/reusable/wrapper";
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
import LocalAPI from "@/service/localAPI";
const FilterGenerator = ({filterPropertiesData}: Pick<InferGetStaticPropsType<typeof getStaticProps>, "filterPropertiesData">) => {
return (
<form className={'filters mb-10'}>
<h2 className={"text-lg mb-4"}>Фильтры</h2>
{filterPropertiesData.map( obj => (
<CheckboxGroup label={obj.name} key={"FILTER_"+obj.id}>
{obj.values && obj.values.map(val => (
<Checkbox value={String(val.id)} key={"VALUE_"+val.id}>{val.value}</Checkbox>
))}
</CheckboxGroup>
))}
</form>
)
}
type CardProps = InferGetStaticPropsType<typeof getStaticProps>['catalog'][0] & {isFavourite?: boolean}
const CatalogCard = (product: CardProps) => {
const isClient = useClient()
const cartItems = useQuery({queryKey: ['cart'], queryFn: async () => {
const service = new LocalAPI()
return await service.getCartItems()
}})
const [imageIsLoading, setImageIsLoading] = useState(true);
const qs = useQueryClient()
const toggleCart = useMutation({
mutationFn: async ({productId, quantity}: { productId: number, quantity: number }) => {
const service = new LocalAPI()
if (cartItems.data!.find(({id}) => id === productId)) {
return await service.deleteCartItem(productId)
}
return await service.addCartItem(productId, quantity)
},
onSuccess: () => {
qs.invalidateQueries({queryKey: ["cart"]})
}
})
const onImageLoad = () => {
setImageIsLoading(false);
}
return (
<div className={"bg-gray-card w-full h-fit min-h-[250px] py-4 px-7 rounded-[20px] hover:shadow-md transition-shadow hover:cursor-pointer"} key={product.id}>
<div className={"grid grid-cols-1 sm:grid-cols-10 w-fit relative items-center"}>
{
imageIsLoading && <Skeleton className={"absolute top-0 left-0 right-3/4 rounded-[20px] h-full"} />
}
{
product.properties.main_image && <Image src={`http://relynolli.elitvinenko.tech/upload/${product.properties.main_image[0]}`} alt={product.properties.viscosity!} className={"col-auto mx-auto mb-4 sm:col-span-2 sm:row-span-2"} width={300} height={430} onLoad={onImageLoad}/>
}
<div className="col-auto sm:col-start-4 sm:col-span-6 h-full flex flex-col justify-center">
<span className={"text-[#52525C] font-normal text-subtitle-4 mb-2 block"}>Стандарт API: {product.properties.api_standart} Тип: {product.properties.oil_type}</span>
<h3 className={"font-bold text-lg uppercase text-black-3"}>{product.name}</h3>
</div>
{
isClient ?
<FavouriteIcon onClick={() => toggleFavourite(product.id)} className={`transition-colors absolute z-20 top-0 right-0 ${product.isFavourite ? "fill-primary" : "fill-[#E0E3E3]"}`}/> : null
}
<div className="flex col-auto row-auto sm:row-start-2 sm:col-start-4 sm:col-span-7 justify-between w-full items-center pt-4">
<span className="font-bold text-xl text-black-3">
{product.price.BASE}
</span>
{
isClient ?
<Button onClick={() => toggleCart.mutate({
productId: product.id,
quantity: 1
})} className={"font-bold text-lg bg-green-2 uppercase italic text-black-3 relative z-20"}>{cartItems.data?.map(({id}) => id).includes(product.id) ? "В корзине" : "В корзину"}</Button> : null
}
<Link href={'/catalog/' + product.code} className={'absolute top-0 left-0 z-10 w-full h-full'}/>
</div>
</div>
</div>
)
}
const Catalog = (props: InferGetStaticPropsType<typeof getStaticProps>) => {
const {favourites} = useSnapshot(favouritesStore)
return (<Wrapper title={"Каталог"} breadcrumbs={[{name: "Каталог", link: "/catalog"}]}>
<div className="flex flex-col justify-between lg:flex-row">
<FilterGenerator filterPropertiesData={props.filterPropertiesData}/>
<div className="products grid grid-cols-1 2xl:grid-cols-2 gap-7 lg:w-3/4 h-fit w-full">
{
props.catalog.map(product =>
<>
<CatalogCard key={product.id} {...product} isFavourite={favourites.includes(product.id)} />
</>
)
}
</div>
</div>
</Wrapper>
)
}
export default Catalog;
export const getStaticProps = async () => {
const {data} = await axios.get<ResponseData[]>("http://localhost:8000/api/v1/catalog/filters")
const {data: catalogData} = await axios.get<ResponseDataProduct[]>("http://localhost:8000/api/v1/catalog")
return {
props: {
filterPropertiesData: data,
catalog: catalogData
}
}
}

148
src/pages/index.tsx Normal file
View File

@ -0,0 +1,148 @@
import {Button, dropdownSection} from "@nextui-org/react";
import ChevronBannerIcon from "@/../public/banner_arr_btn.svg.svg"
import {Card, CardHeader, Image} from "@nextui-org/react";
import Link from "next/link";
import NextImage from "next/image";
const Hero = () => {
return (
<section className={"w-full bg-cars bg-no-repeat h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right"}>
<div className={"wrapper h-full flex flex-col justify-center"}>
<div className="w-11/12 mx-auto lg:m-0 xl:w-1/2 flex flex-col bg-black bg-opacity-25 py-4 xl:px-7 px-2 rounded-[20px]">
<h2 className={"text-2xl leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-[52px]"}>Оформите заказ
на сумму от 5000</h2>
<span className={"text-sm xl:text-subtitle-1 leading-inherit text-subtitle-1 mb-4 xl:mb-24"}>До 15 января и получите бесплатную доставку</span>
<Link href={"/catalog"} className={'w-full lg:w-1/2 bg-green-2 rounded-[8px] p-6 flex justify-between items-center text-black hover:scale-105 transition'}>
<span className={"text-sm text-[1.125rem] font-extrabold italic uppercase"}>Перейти к покупке</span>
<ChevronBannerIcon className={"stroke-[3px] stroke-black"} />
</Link>
<span className={"text-white opacity-60 text-sm mt-2"}>*Срок действия акции ограничен</span>
</div>
</div>
</section>
)
}
const MainInfo = () => {
return (
<section className={"bg-white text-black rounded-[8px]"}>
<div className="wrapper py-8 mb-8">
<h1 className={"text-4xl xl:text-title-1 leading-normal font-bold text-center italic uppercase text-black mb-8"}>Моторные масла и смазочные материалы <br/>
<span className={"text-primary"}>Relynolli ®</span></h1>
<span className={"text-base lg:text-2xl xl:text-4xl text-center block w-1/2 mx-auto"}>Технологичные решения для надежной работы двигателя</span>
</div>
<div className="wrapper mx-auto py-8 grid grid-cols-1 lg:grid-cols-2 grid-rows-3 lg:grid-rows-2 gap-4 h-auto [&>*]:h-[390px] [&_span]:text-base [&_span]:md:text-xl [&_span]:leading-[110%]">
<Card className={"rounded-[30px] hover:scale-105 transition-size"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil2.png"}></Image>
<CardHeader
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
<h3 className={"text-title-3 leading-[35px] text-white"}>Масла для легковых автомобилей</h3>
<span className={"text-gray-2"}>Серия Standart и Premium для легковых автомобилей для наилучшей энергоэффективности двигателя вашего автомобиля</span>
</CardHeader>
</Card>
<Card className={"rounded-[30px] hover:scale-105 transition-size"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil1.png"}></Image>
<CardHeader
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
<h3 className={"text-title-3 leading-[35px] text-white"}>Специальная серия масел XMR</h3>
<span className={"text-gray-2"}>Серия моторных масел XMR - премиальная линейка для двигателей со спортивным характером</span>
</CardHeader>
</Card>
<Card className={"rounded-[30px] hover:scale-105 transition-size"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil3.png"}></Image>
<CardHeader
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
<h3 className={"text-title-3 leading-[35px] text-white"}>Масла для коммерческого транспорта и спецтехники</h3>
<span className={"text-gray-2"}>Синтетические и полусинтетические масла для двигателей, которые эксплуатируются под высокими нагрузками. Подходят для бензиновых и дизельных двигателей</span>
</CardHeader>
</Card>
<Card className={"rounded-[30px] hover:scale-105 transition-size"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil4.png"}></Image>
<CardHeader
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
<h3 className={"text-title-3 leading-[35px] text-white"}>Масла для мототехники</h3>
<span className={"text-gray-2"}>Масла для четырех и двухтактных двигателей, которые помогут раскрыть неудержимый характер вашей мототехники</span>
</CardHeader>
</Card>
</div>
</section>
)
}
const Achievements = () => {
return (
<section className={"bg-cover backdrop-brightness-50 relative"}>
<Image removeWrapper alt={"oiltypeImage"} className={"absolute top-0 left-0 w-full h-full brightness-50 object-fill -z-10"} src={"/oiltypeImage.png"}></Image>
<div className="wrapper py-8 mb-8 flex flex-col justify-center">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6 [&>*]:h-[500px] [&>*]:md:h-[700px]">
<Card className={"z-10 relative rounded-[30px] font-bold [&_span]:font-normal hover:scale-105 transition-size"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover"}
src={"/achievements1.png"}></Image>
<CardHeader
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-1/2 justify-between">
<div className="block">
<h3 className={"text-2xl md:text-3xl leading-[35px] text-white mb-6 "}>
Расшифровка маркировки масел группы-М</h3>
<span className={"text-base md:text-xl text-white opacity-50"}>Значительно увеличивает ресурс вашего двигателя</span>
</div>
<Button className={"bg-green-2 font-bold uppercase italic"} endContent={<ChevronBannerIcon
className={"stroke-[3px] stroke-black"}/>}>Посмотреть</Button>
</CardHeader>
</Card>
<Card className={"z-10 relative rounded-[30px] font-bold [&_span]:font-normal hover:scale-105 transition-size"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover"}
src={"/achievements2.png"}></Image>
<CardHeader
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-1/2 justify-between">
<div className="block">
<h3 className={"text-2xl md:text-3xl leading-[35px] text-white mb-6 "}>Моторные масла Relynolli ®</h3>
<span className={"text-base md:text-xl text-white opacity-50"}>Обладают высокой смазывающей способностью и обеспечивают надёжную защиту двигателя от износа</span>
</div>
<Button className={"bg-green-2 font-bold uppercase italic"} endContent={<ChevronBannerIcon
className={"stroke-[3px] stroke-black"}/>}>Расшифровка масел группы - N</Button>
</CardHeader>
</Card>
</div>
<Button className={"bg-green-2 font-bold uppercase italic mx-auto"} endContent={<ChevronBannerIcon className={"stroke-[3px] stroke-black"}/>}>Перейти в продукцию</Button>
</div>
</section>)
}
export default function Home() {
return (
<>
<Hero/>
<MainInfo/>
<Achievements/>
</>
);
}

357
src/pages/order/make.tsx Normal file
View File

@ -0,0 +1,357 @@
import Wrapper from "@/components/reusable/wrapper";
import {Input} from "@nextui-org/input";
import {Radio, RadioGroup, RadioProps} from "@nextui-org/radio";
import {Autocomplete, AutocompleteItem, Button, CircularProgress, cn, Tooltip} from "@nextui-org/react";
import DeliveryIcon from "@/../public/delivery.svg"
import StorageIcon from "@/../public/storage.svg"
import axios from "axios";
import {useQuery, useQueryClient} from "@tanstack/react-query";
import {HTMLProps, useEffect, useRef, useState} from "react";
import {useForm, Controller, SubmitHandler} from "react-hook-form";
import {cmp} from "semver";
import OrderInfo from "@/components/pages/cart/orderInfo";
import {useRouter} from "next/navigation";
import {Modal, ModalBody, ModalContent, ModalHeader} from "@nextui-org/modal";
import {For} from "@babel/types";
import {getUserId} from "@/store/cart";
import {nanoid} from "nanoid";
const getTakeAwayInfo = async () => {
return (await axios.get("/api/v1/delivery/3")).data
}
const normalizeInput = (value: string, previousValue: string) => {
// return nothing if no value
if (!value) return value;
// only allows 0-9 inputs
const currentValue = value.replace(/[^\d]/g, '');
const cvLength = currentValue.length;
if (!previousValue || value.length > previousValue.length) {
if (cvLength < 2) return `+${currentValue} `;
// returns: "x", "xx", "xxx" "xxx"
if (cvLength < 5) return `+${currentValue.slice(0, 1)} ${currentValue.slice(1)}`;
// 7 (902) 486 65-00
// returns: "(xxx)", "(xxx) x", "(xxx) xx", "(xxx) xxx",
if (cvLength < 8) return `+${currentValue.slice(0, 1)} (${currentValue.slice(1, 4)}) ${currentValue.slice(4)}`;
// returns: "(xxx) xxx-", (xxx) xxx-x", "(xxx) xxx-xx", "(xxx) xxx-xxx", "(xxx) xxx-xx"
if (cvLength < 10) return `+${currentValue.slice(0, 1)} (${currentValue.slice(1, 4)}) ${currentValue.slice(4, 7)} ${currentValue.slice(7)}`;
return `+${currentValue.slice(0, 1)} (${currentValue.slice(1, 4)}) ${currentValue.slice(4, 7)} ${currentValue.slice(7, 9)}-${currentValue.slice(9)}`;
}
return ""
};
type FormDataValuesType = {
fullName: string
phoneNumber: string
email: string
receivingMethod: string
address?: string
deliveryTypeId?: number
paymentTypeId: number
comment: string
}
const DeliveryTypeInput = ({deliveryType, ...formProps}: {deliveryType: "take-away" | "delivery"} & RadioProps) => {
if (deliveryType === 'take-away') return (
<div className={"flex-[1_1_calc((100%_/_2)_-_20px)]"}>
<Radio className={""}
classNames={{
label: cn("flex flex-row gap-10"),
labelWrapper: cn("w-full"),
wrapper: cn("hidden"),
base: cn("!max-w-full border-[2px] border-[#8F8F8F] p-5 rounded-[20px] data-[selected=true]:border-primary data-[selected=true]:bg-primary transition-colors")
}} {...formProps} value={"take-away"}>
<StorageIcon className={"group-data-[selected=true]:fill-[#151515] fill-[#8F8F8F]"}/>
<div className="text self-stretch flex justify-between flex-col py-2 w-1/3">
<h3 className={"text-subtitle-3 font-bold group-data-[selected=true]:text-[#151515] text-[#8F8F8F]"}>Самовывоз</h3>
<p className={"text-subtitle-4 text-[#8F8F8F] group-data-[selected=true]:text-[#151515]"}>
г. Домодедово
ул. Каширское Шоссе д. 4 к.1
</p>
</div>
</Radio>
</div>
)
else return (
<Tooltip content={"В разработке"} className={"text-black-2"}>
<div className={"flex flex-[1_1_calc((100%_/_2)_-_20px)]"}>
<Radio
classNames={{
label: cn("flex flex-row gap-10"),
labelWrapper: cn("w-full"),
wrapper: cn("hidden"),
base: cn("!max-w-full border-[2px] border-[#8F8F8F] p-5 rounded-[20px] data-[selected=true]:border-primary data-[selected=true]:bg-primary transition-colors w-full")
}} isDisabled={true} {...formProps} value={"delivery"}>
<DeliveryIcon className={"group-data-[selected=true]:fill-[#151515] fill-[#8F8F8F]"}/>
<div className="text self-stretch flex justify-between flex-col py-2 w-1/3">
<h3 className={"text-subtitle-3 font-bold group-data-[selected=true]:text-[#151515] text-[#8F8F8F]"}>Доставка</h3>
<p className={"text-subtitle-4 text-[#8F8F8F] group-data-[selected=true]:text-[#151515]"}>
Доставка с помощью ТК
</p>
</div>
</Radio>
</div>
</Tooltip>
)
}
const MakeOrder = () => {
const qc = useQueryClient();
const router = useRouter();
const [modalVisible, setModalVisible] = useState(false);
const submitForm: SubmitHandler<FormDataValuesType> = async (data) => {
setModalVisible(true)
const {data: responseData} = await axios.post('/api/v1/order/make', {
fuserId: await getUserId(),
fullName: data.fullName,
email: data.email,
phoneNumber: data.phoneNumber
})
router.push(responseData.confirmation.confirmation_url)
}
const takeawayInfo = useQuery({
queryKey: ["takeaway_info"],
queryFn: getTakeAwayInfo
})
const {control, handleSubmit, formState: {errors}, watch, setValue} = useForm<FormDataValuesType>({
mode: "all",
defaultValues: {}
})
const [phoneNumberPrev, setPhoneNumberPrev] = useState("")
const phoneNumberCur = watch("phoneNumber")
const receivingMethodCur = watch("receivingMethod")
// const formRef = useRef<HTMLFormElement>(null)
useEffect(() => {
if (phoneNumberCur === phoneNumberPrev) return
setValue('phoneNumber', normalizeInput(phoneNumberCur, phoneNumberPrev))
setPhoneNumberPrev(phoneNumberCur)
}, [phoneNumberCur])
const [isDisabled, setIsDisabled] = useState(false)
const formId = nanoid(5)
return (
<Wrapper title={"Офромление заказа"}
breadcrumbs={[{name: "Корзина", link: "/cart"}, {name: "Оформление заказа", link: "/order/make"}]}>
<div className="flex w-full pb-16 justify-between">
<form id={formId} className={"text-[#151515] [&>div]:mb-5 w-8/12"} onSubmit={handleSubmit(submitForm)}>
<div className="form__group bg-gray-card p-7 rounded-[30px]">
<h2 className={"mb-5 font-bold text-subtitle-2"}>Покупатель</h2>
<div className="form__fields grid grid-cols-2 gap-5">
<Controller name={"fullName"} control={control}
rules={{required: "Поле обязательно для заполнения"}}
render={({field}) =>
<Input className={"col-span-2"}
classNames={{
"inputWrapper": "h-[65px]",
"label": "group[data-filled-within=true] group-data-[filled-within=true]:-translate-y-[60px] group-data-[filled-within=true]:text-[#8F8F8F]"
}}
variant={"bordered"} label={"ФИО"} type={"text"} isRequired
isInvalid={!!errors.fullName}
errorMessage={errors.fullName && errors.fullName.message}
labelPlacement={"outside"} {...field}/>
}
/>
<Controller control={control}
name={"phoneNumber"}
rules={{
required: "Поле обязательно для заполнения",
pattern: {
message: "Неверный формат номера телефона",
value: /\+\d \(\d{3}\) \d{3} \d{2}-\d{2}/
}
}}
render={({field}) =>
<Input
classNames={{
"inputWrapper": "h-[65px]",
"label": "group[data-filled-within=true] group-data-[filled-within=true]:-translate-y-[60px] group-data-[filled-within=true]:text-[#8F8F8F]"
}}
variant={"bordered"} label={"Телефон"} type={"tel"} isRequired
labelPlacement={"outside"} {...field}
isInvalid={!!errors.phoneNumber}
errorMessage={errors.phoneNumber && errors.phoneNumber.message}
/>
}/>
<Controller
control={control}
name={"email"}
rules={{
required: "Поле обязательно для заполнения", pattern: {
value: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/,
message: "Неверный формат эл. почты"
}
}}
render={({field}) =>
<Input
classNames={{
"inputWrapper": "h-[65px]",
"label": "group[data-filled-within=true] group-data-[filled-within=true]:-translate-y-[60px] group-data-[filled-within=true]:text-[#8F8F8F]"
}}
variant={"bordered"} label={"E-mail"} type={"email"} isRequired
labelPlacement={"outside"} {...field}
isInvalid={!!errors.email}
errorMessage={errors.email && errors.email.message}
/>
}
/>
</div>
</div>
<div className="form__group bg-gray-card p-7 rounded-[30px]">
<h2 className={"mb-5 font-bold text-subtitle-2"}>Способ получения товара <span
className={"text-red-500"}>*</span></h2>
<RadioGroup classNames={{"wrapper": cn("flex flex-row justify-center gap-10")}}
// onValueChange={(value) => {
// setValue("receivingMethod", value);
// value === "take-away" ? setValue("deliveryTypeId", 3) : setValue("deliveryTypeId", undefined)
// }}
>
<Controller control={control} name={"receivingMethod"} render={({field}) =>
<DeliveryTypeInput deliveryType={"take-away"} {...field} />} />
<Controller
control={control}
name={"receivingMethod"}
render={({field}) =>
<DeliveryTypeInput deliveryType={"delivery"} {...field} />} />
</RadioGroup>
</div>
{
receivingMethodCur === 'take-away' && !takeawayInfo.error &&
<div className={"bg-gray-card p-7 rounded-[30px]"}>
<h2 className={"mb-5 font-bold text-subtitle-2"}>{takeawayInfo.data!.name}</h2>
<div className="content"
dangerouslySetInnerHTML={{__html: takeawayInfo.data!.description}}></div>
</div>
}
{
receivingMethodCur === 'delivery' &&
<>
<div className="form__group bg-gray-card p-7 rounded-[30px]">
<h2 className={"mb-5 font-bold text-subtitle-2"}>Адрес доставки</h2>
<div className="form__fields grid grid-cols-2 gap-5">
<Autocomplete className={"col-span-2"} label={"Адрес"} labelPlacement={"outside"}
variant={"bordered"} isRequired
inputProps={{
classNames: {
inputWrapper: cn("h-[65px]"),
label: cn("group[data-filled-within=true] group-data-[filled-within=true]:-translate-y-[60px] group-data-[filled-within=true]:text-[#8F8F8F]"),
},
}}
>
<AutocompleteItem classNames={{
title: cn("text-[#8F8F8F] data-[selected=true]:text-[#151515]"),
}} key={'moscow'} value={"moscow"}>Москва</AutocompleteItem>
</Autocomplete>
</div>
</div>
<div className="form__group bg-gray-card p-7 rounded-[30px]">
<h2 className={"mb-5 font-bold text-subtitle-2"}>Служба доставки</h2>
<div className="form__fields">
</div>
</div>
</>
}
<div className={"form__group bg-gray-card p-7 rounded-[30px]"}>
<h2 className={"mb-5 font-bold text-subtitle-2"}>Способ оплаты</h2>
<div className="form__fields">
<RadioGroup
className={"flex flex-col gap-5"}
>
{/*<Radio value={"cash"} className={"flex-[1_1_calc((100%_/_2)_-_20px)]"} >Оплата наличными</Radio>*/}
<Radio value={"online"} className={"flex-[1_1_calc((100%_/_2)_-_20px)]"}>Оплата картами,
SberPay, другие системы</Radio>
</RadioGroup>
</div>
</div>
</form>
<Modal isOpen={modalVisible} size={'4xl'}>
<ModalContent className={"text-[#151515]"}>
<ModalBody>
<div className="flex items-center flex-col p-7">
<h2 className={"text-title-3 text-center mb-4"}>Подождите, ваш заказ
обрабатывается.</h2>
<span className={"text-subtitle-3 mb-7"}>
Вас скоро перенаправит на страницу оплаты
</span>
<CircularProgress size={'lg'}/>
</div>
</ModalBody>
</ModalContent>
</Modal>
<div className="flex flex-col w-[30%]">
<OrderInfo setIsDisabled={setIsDisabled} isDisabled={isDisabled}/>
<div className="flex">
<Button type={"submit"} form={formId} color={"primary"}
className={"text-subtitle-3 uppercase italic font-bold h-[70px] w-full"}
isDisabled={isDisabled}>Оформить заказ</Button>
</div>
</div>
</div>
</Wrapper>
)
}
export default MakeOrder;

120
src/service/bitrixAPI.ts Normal file
View File

@ -0,0 +1,120 @@
import axios, {AxiosInstance} from "axios";
import Db from "@/service/db"
type CreateOrderRequestData = {
result:
{
order: {
id: number
}
}
}
export default class BitrixAPIBase {
protected instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: "https://tehnohimgrupp.ru/rest/100/tkos864yczl1mahk",
headers: {
"Content-Type": "application/json",
}
});
}
async createOrderRequest(additionalInfo: string) {
const {data} = await this.instance.post<CreateOrderRequestData>("/sale.order.add", {
"fields": {
"lid": "s2",
"personTypeId": 5,
"currency": "RUB",
"comments": additionalInfo
}
})
return data.result.order.id
}
async approvePayment(paymentId: number, paySystemId: number) {
try {
const {data} = await this.instance.post('/sale.payment.update', {
id: paymentId,
fields: {
paid: 'Y',
paySystemId
}
})
}
catch (e) {
console.log(e)
}
}
async cancelOrder(orderId: number) {
const {data} = await this.instance.post('/sale.order.update', {
id: orderId,
fields: {
"canceled": "Y"
}
})
}
async getOrderInfo(orderId: number) {
const {data} = await this.instance.post(`/sale.order.get`, {
"id": orderId
})
return data.result.order
}
async createPayment(orderId: number, sum: number) {
const {data} = await this.instance.post('/sale.payment.add', {
fields: {
orderId,
sum,
paid: 'N',
paySystemId: 8,
}
})
}
async addProductsToOrder(orderId: number, fuserId: number) {
const stmt = `select t1.product_id as product_id, t1.price_type_id as price_type_id, t1.quantity as quantity, t2.price->>'$.BASE' as price from api_cart t1 join api_catalog t2 on t1.product_id = t2.id where fuser_id = ${fuserId};`
const [result]: [result: {product_id: number, price_type_id: number, quantity: number, price: number}[]] = (await Db.query(stmt)) as any
for (const elem of result) {
await this.instance.post("/sale.basketitem.addCatalogProduct", {
"fields": {
orderId,
"productId": +elem.product_id,
"priceTypeId": +elem.price_type_id,
"quantity": +elem.quantity,
"currency": "RUB",
"price": +elem.price
}
})
}
}
async modifyPropertyValuesForOrder(orderId: number, props: {orderPropsId: number, value: string}[]) {
const {data} = await this.instance.post('/sale.propertyvalue.modify', {
fields: {
order: {
id: orderId,
propertyValues: props
}
}
})
}
}

30
src/service/db.ts Normal file
View File

@ -0,0 +1,30 @@
import mysql from 'mysql2/promise'
class DB {
pool: mysql.Pool | null = null
constructor() {
this.createConnection()
}
createConnection() {
this.pool = mysql.createPool({
connectionLimit: 5,
host: 'relynolli.ru',
user: 'ernlitvinenko',
password: 'Ernest080503qq',
database: 'sitemanager'
})
}
async query(sql: string) {
const con = await this.pool?.getConnection()
return await con!.query(sql);
}
}
const inst = new DB()
export default inst;

84
src/service/localAPI.ts Normal file
View File

@ -0,0 +1,84 @@
import axios, {AxiosInstance} from "axios";
type createFUserType = {
fuserId: number
}
type getCartItemsType = {
id: number,
code: string,
name: string,
is_active: number,
properties: {
[key: string]: string
}
quantity: number,
available_quantity: number
}
class LocalAPI {
private instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: 'http://localhost:8000'
})
}
async getFuserId() {
let fuserId: string | number | null = localStorage.getItem('fuserId')
if (!fuserId) {
fuserId = (await this.createFUser()).fuserId
localStorage.setItem('fuserId', JSON.stringify(fuserId))
}
console.log("current fuserId", fuserId)
return +fuserId
}
async createFUser() {
const {data} = await this.instance.post<createFUserType>('/api/v1/cart')
console.log("Fuser id is", data)
return data
}
async getCartItems() {
const {data} = await this.instance.get<getCartItemsType[]>('/api/v1/cart', {
params: {
fuserId: await this.getFuserId()}
})
return data
}
async addCartItem(productId: number, quantity: number) {
const {data} = await this.instance.post('/api/v1/cart/item', {productId, quantity, fuserId: await this.getFuserId(), priceTypeId: 1})
return data
}
async changeQuantity(productId: number, quantity: number) {
try {
const {data} = await this.instance.patch('/api/v1/cart/item', {productId, quantity, fuserId: await this.getFuserId()})
return data
}
catch (e) {
console.log(e)
}
}
async deleteCartItem(productId: number) {
const {data} = await this.instance.delete('/api/v1/cart/item', {data: {productId, fuserId: await this.getFuserId()}})
return data
}
async totalProductPrice() {
const {data} = await this.instance.post('/api/v1/order/total', {fuserId: await this.getFuserId()})
return data
}
}
export default LocalAPI;

View File

@ -0,0 +1,75 @@
import axios, {AxiosInstance} from "axios";
import {randomUUID} from "crypto";
type PaymentData = {
id: string,
status: string,
paid: boolean,
confirmation: {
confirmation_url: string
}
}
export default class YookassaAPIBase {
protected instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: "https://api.yookassa.ru/v3/",
headers: {
"Content-Type": "application/json",
},
auth: {
username: "236968",
password: "live_yB4Mhu6CVeeGo3XdFGpZ1hbnEXa6dgPC9cAjHToGr9s"
}
});
}
async createPayment(orderId: number, sum: number, items: {description: string, amount: number, quantity: number}[]) {
const itemsQuery = items.map(item => {
return {
"description": item.description,
"amount": {
"value": `${item.amount}`,
"currency": "RUB"
},
"vat_code": 4,
"quantity": item.quantity,
"measure": "piece",
"payment_subject": "commodity",
"payment_mode": "full_payment"
}
})
const query = {
"amount": {
"value": `${sum}`,
"currency": "RUB"
},
"capture": true,
"receipt" : {
"customer" : {
"email": "ernest@elitvinenko.tech",
"phone": "79024866500"
},
"items": itemsQuery,
"tax_system_code": 1
},
"confirmation": {
"type": "redirect",
"return_url": "https://relynolli.ru/"
},
"description": `Заказ №${orderId}`
}
const {data} = await this.instance.post<PaymentData>('/payments', query, {
headers: {
"Idempotence-Key": randomUUID()
}
})
return data
}
}

86
src/store/cart.ts Normal file
View File

@ -0,0 +1,86 @@
import {proxy} from "valtio";
import axios from "axios";
import {z} from "zod";
import {ResponseData} from "@/pages/api/v1/catalog";
type CardStoreType = {
fuserId: number | null
items: Array<ResponseData & { quantity: number }>
}
const cart = proxy<CardStoreType>({
fuserId: null,
items: []
})
export default cart
export const toggleCart = async (product: ResponseData) => {
if (!cart.fuserId) {
cart.fuserId = await getUserId()
}
if (cart.items.some(item => item.id === product.id)) {
cart.items = cart.items.filter(item => item.id !== product.id)
await axios.delete('/api/v1/cart/item', {
data: {
fuserId: cart.fuserId,
productId: product.id
}
})
} else {
cart.items.push({...product, quantity: 1})
await axios.post('/api/v1/cart/item', {
fuserId: cart.fuserId,
productId: product.id,
quantity: 1,
})
}
}
export const getCartItems = async () => {
if (!cart.fuserId) {
cart.fuserId = await getUserId()
}
const {data} = await axios.get('/api/v1/cart', {
params: {
fuserId: cart.fuserId
}
})
cart.items = data
}
export const getUserId = async () => {
if (typeof localStorage !== 'undefined' && !localStorage.getItem('fuserId')) {
const fuserId = (await axios.post<{ fUserId: number }>('/api/v1/cart')).data.fUserId
localStorage.setItem('fuserId', JSON.stringify(fuserId))
return fuserId
}
return Number(localStorage.getItem('fuserId'))
}
export const changeQuantity = async (id: number, quantity: number) => {
if (quantity == 0) {
cart.items = cart.items.filter(elem => elem.id !== id)
await axios.delete('/api/v1/cart/item', {
data: {
fuserId: cart.fuserId,
productId: id,
}
})
return
}
cart.items.forEach(elem => elem.id === id && (elem.quantity = quantity))
await axios.patch('/api/v1/cart/item', {
fuserId: cart.fuserId,
productId: id,
quantity
})
}

20
src/store/favourites.ts Normal file
View File

@ -0,0 +1,20 @@
import {proxy} from "valtio";
type favouritesData = {favourites: number[]};
const favouritesStore = proxy({
favourites: typeof localStorage !== "undefined" ? JSON.parse(localStorage.getItem("favourites") || '[]') : []
})
export const toggleFavourite = (id: number) => {
if (favouritesStore.favourites.includes(id)) {
favouritesStore.favourites = favouritesStore.favourites.filter((favourite: number) => favourite !== id)
}
else {
favouritesStore.favourites = [...favouritesStore.favourites, id]
}
localStorage.setItem("favourites", JSON.stringify(favouritesStore.favourites))
}
export default favouritesStore;

26
src/styles/globals.css Normal file
View File

@ -0,0 +1,26 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
background-color: #000;
}
html, body {
height: 100%;
@apply bg-black-4 text-white;
}
body {
overflow-x: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.wrapper {
@apply 2xl:max-w-[1440px] max-w-full sm:max-w-[540px] md:max-w-[720px] lg:max-w-[960px] xl:max-w-[1140px] mx-auto;
}
}

74
tailwind.config.ts Normal file
View File

@ -0,0 +1,74 @@
import type { Config } from "tailwindcss";
const {nextui} = require("@nextui-org/react");
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-linear-green": "linear-gradient(180deg, #92E727 0%, #000000 100%)",
"cars": "url(\"/banner.png\")",
"oil-type": "url(\"/oilTypeImage.png\")"
},
colors: {
"green-1": "#B3C53F",
"green-2": "#92E727",
"yellow-1": "#FFD235",
"gray-1": "#ccc",
"gray-2": "#cacaca",
"gray-3": "#787878",
"black-1": "#000",
"black-2": "#262626",
},
fontSize: {
"header-link": "1.0625rem",
"header-phone": '1rem',
"title-1": "5rem",
"title-2": "4.375rem",
"title-3": "2.1875rem",
"title-4": "2.5rem",
"title-5": "1.25rem",
"subtitle-1": "2.0625rem",
"subtitle-2": "1.75rem",
"subtitle-3": "1.1875rem",
"subtitle-4": "1rem",
"subtitle-5": "0.875rem",
base: "1rem",
},
fontFamily: {
mulish: ["var(--font-mulish)", "sans-serif"],
}
},
},
plugins: [nextui(
{themes: {
light: {
colors: {
"primary": "#92E727",
"green-1": "#B3C53F",
"green-2": "#92E727",
"yellow-1": "#FFD235",
"gray-card": "#F7F6F8",
"gray-1": "#ccc",
"gray-2": "#cacaca",
"gray-3": "#787878",
"black-1": "#000",
"black-2": "#262626",
"black-3": "#151515",
"black-4": "#1D1D1E",
"warning": "#FFD235",
},
},
}}
)],
};
export default config;

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

6325
yarn.lock Normal file

File diff suppressed because it is too large Load Diff