Compare commits
No commits in common. "master" and "hotfix/hotfix-card-image" have entirely different histories.
master
...
hotfix/hot
|
@ -1,6 +0,0 @@
|
||||||
/** @type {import('next-sitemap').IConfig} */
|
|
||||||
module.exports = {
|
|
||||||
siteUrl: 'https://relynolli.ru',
|
|
||||||
generateRobotsTxt: true, //
|
|
||||||
// ...other options
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
reactStrictMode: false,
|
reactStrictMode: true,
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
|
@ -17,32 +17,32 @@ const nextConfig = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
const fileLoaderRule = config.module.rules.find((rule) =>
|
const fileLoaderRule = config.module.rules.find((rule) =>
|
||||||
rule.test?.test?.('.svg'),
|
rule.test?.test?.('.svg'),
|
||||||
)
|
)
|
||||||
|
|
||||||
config.module.rules.push(
|
config.module.rules.push(
|
||||||
// Reapply the existing rule, but only for svg imports ending in ?url
|
// Reapply the existing rule, but only for svg imports ending in ?url
|
||||||
{
|
{
|
||||||
...fileLoaderRule,
|
...fileLoaderRule,
|
||||||
test: /\.svg$/i,
|
test: /\.svg$/i,
|
||||||
resourceQuery: /url/, // *.svg?url
|
resourceQuery: /url/, // *.svg?url
|
||||||
},
|
},
|
||||||
// Convert all other *.svg imports to React components
|
// Convert all other *.svg imports to React components
|
||||||
{
|
{
|
||||||
test: /\.svg$/i,
|
test: /\.svg$/i,
|
||||||
issuer: fileLoaderRule.issuer,
|
issuer: fileLoaderRule.issuer,
|
||||||
resourceQuery: {not: [...fileLoaderRule.resourceQuery.not, /url/]}, // exclude if *.svg?url
|
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url
|
||||||
use: ['@svgr/webpack'],
|
use: ['@svgr/webpack'],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Modify the file loader rule to ignore *.svg, since we have it handled now.
|
||||||
|
fileLoaderRule.exclude = /\.svg$/i
|
||||||
|
|
||||||
// Modify the file loader rule to ignore *.svg, since we have it handled now.
|
return config
|
||||||
fileLoaderRule.exclude = /\.svg$/
|
}
|
||||||
return config
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
13
package.json
|
@ -6,18 +6,12 @@
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint"
|
||||||
"postbuild": "next-sitemap"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cdek-it/widget": "3",
|
|
||||||
"@nextui-org/react": "^2.2.9",
|
"@nextui-org/react": "^2.2.9",
|
||||||
"@pbe/react-yandex-maps": "^1.2.5",
|
|
||||||
"@tanstack/react-query": "^5.20.5",
|
"@tanstack/react-query": "^5.20.5",
|
||||||
"@tanstack/react-query-devtools": "^5.21.7",
|
"@tanstack/react-query-devtools": "^5.21.7",
|
||||||
"@types/lodash": "^4.17.0",
|
|
||||||
"@types/yandex-maps": "^2.1.35",
|
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"framer-motion": "^11.0.3",
|
"framer-motion": "^11.0.3",
|
||||||
"libphonenumber-js": "^1.10.56",
|
"libphonenumber-js": "^1.10.56",
|
||||||
|
@ -25,15 +19,11 @@
|
||||||
"mysql2": "^3.9.1",
|
"mysql2": "^3.9.1",
|
||||||
"nanoid": "^5.0.5",
|
"nanoid": "^5.0.5",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"next-sitemap": "^4.2.3",
|
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.50.1",
|
"react-hook-form": "^7.50.1",
|
||||||
"react-image": "^4.1.0",
|
"react-image": "^4.1.0",
|
||||||
"react-imask": "^7.5.0",
|
"react-imask": "^7.5.0",
|
||||||
"react-stately": "^3.30.1",
|
|
||||||
"react-ymaps3": "^0.0.19",
|
|
||||||
"swiper": "^11.0.7",
|
|
||||||
"valtio": "^1.13.0",
|
"valtio": "^1.13.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
|
@ -43,7 +33,6 @@
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"@yandex/ymaps3-types": "^0.0.24",
|
|
||||||
"autoprefixer": "^10.0.1",
|
"autoprefixer": "^10.0.1",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.1.0",
|
"eslint-config-next": "14.1.0",
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="40" height="40" rx="8" fill-opacity="0.7"/>
|
|
||||||
<g clip-path="url(#clip0_948_14736)">
|
|
||||||
<path d="M31.4985 14.1546C31.2225 13.1096 30.4092 12.2868 29.3765 12.0075C27.505 11.5 20 11.5 20 11.5C20 11.5 12.4949 11.5 10.6234 12.0075C9.59068 12.2868 8.77741 13.1096 8.50141 14.1546C8 16.0485 8 20 8 20C8 20 8 23.9514 8.50141 25.8454C8.77741 26.8903 9.59068 27.7132 10.6234 27.9926C12.4949 28.5 20 28.5 20 28.5C20 28.5 27.505 28.5 29.3765 27.9926C30.4092 27.7132 31.2225 26.8903 31.4985 25.8454C32 23.9514 32 20 32 20C32 20 32 16.0485 31.4985 14.1546Z" />
|
|
||||||
<path class="gray" d="M17.709 24.0737V16.625L23.629 20.3495L17.709 24.0737Z"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_948_14736">
|
|
||||||
<rect width="24" height="17" fill="white" transform="translate(8 11.5)"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 864 B |
BIN
public/cars.PNG
Before Width: | Height: | Size: 1.8 MiB |
BIN
public/hands.PNG
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 8.6 KiB |
|
@ -1,9 +0,0 @@
|
||||||
# *
|
|
||||||
User-agent: *
|
|
||||||
Allow: /
|
|
||||||
|
|
||||||
# Host
|
|
||||||
Host: https://relynolli.ru
|
|
||||||
|
|
||||||
# Sitemaps
|
|
||||||
Sitemap: https://relynolli.ru/sitemap.xml
|
|
|
@ -1,36 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
|
||||||
<url><loc>https://relynolli.ru</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/articles</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/cart</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/contact</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/order/make</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-lh-10w-40-sn-cf-1l-kanistra-acea-a5-b5</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-lh-10w-40-sn-cf-4l-kanistra-acea-a5-b5</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-30-sn-cf-1l-kanistra-acea-a5-b5</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-30-sn-cf-205l-bochka-acea-a5-b5</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-30-sn-cf-4l-kanistra-acea-a5-b5</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-40-sn-cf-1l-kanistra-acea-a5-b5</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-40-sn-cf-205l-bochka-acea-a5-b5</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog/maslo-motornoe-relynolli-premium-m1-nh-5w-40-sn-cf-4l-kanistra-acea-a5-b5</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog/maslo-motornoe-relynolli-standart-m1-lh-10w-40-slcf-1l-kanistra-acea-a3b4</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/catalog/maslo-motornoe-relynolli-standart-m1-lh-10w-40-slcf-4l-kanistra-acea-a3b4</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/articles/poyasnenie-po-sootvetstviyu-masel-relynolli-klassu-acea-a5-b5</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/articles/professionalnoe-retsenzirovanie-dissertatsii-ot-ntts-td-tekhnokhim-grupp</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/articles/tsifrovizatsiya-sklada-otgruzki-bez-oshibok-relynolli</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/articles/realnye-ispytaniya-masla-ili-fokus-pokus</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/articles/nauchno-tekhnicheskiy-tsentr-vozmozhnosti-i-perspektivy</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/articles/skhema-ispytaniy-relynolli-garantiya-nadezhnosti-i-dostovernosti</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news/meniaj_besplatno</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news/tv-reportazh-relynolli</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news/motornoe-maslo-relynolli-v-podarok</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news/samarskij-politeh-relynolli</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news/start-sotrudnichestva-service-trans-cargo-tehnohim-relynolli</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news/tehnohim-yarmarka-vakanciy</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news/skidka-10-dlya-podpischikov-kanala-telegram</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news/kazautoexpo-2023-relynolli</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news/relynolli-magazin-roznica</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
<url><loc>https://relynolli.ru/news/bisness-yaroslavii</loc><lastmod>2024-04-15T14:24:16.500Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
|
||||||
</urlset>
|
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
||||||
<sitemap><loc>https://relynolli.ru/sitemap-0.xml</loc></sitemap>
|
|
||||||
</sitemapindex>
|
|
BIN
public/sto.PNG
Before Width: | Height: | Size: 2.3 MiB |
|
@ -1,21 +0,0 @@
|
||||||
<svg width="45" height="40" viewBox="0 0 45 40" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="45" height="40" rx="8" fill-opacity="0.7"/>
|
|
||||||
<g clip-path="url(#clip0_79_274)">
|
|
||||||
<g clip-path="url(#clip1_79_274)">
|
|
||||||
<g clip-path="url(#clip2_79_274)">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.1733 19.0227C19.7338 17.0385 22.7748 15.7304 24.2964 15.0984C28.6408 13.2939 29.5436 12.9804 30.132 12.9701C30.2614 12.9678 30.5508 12.9998 30.7382 13.1517C30.8965 13.28 30.94 13.4532 30.9609 13.5748C30.9816 13.6964 31.0076 13.9734 30.987 14.1898C30.7515 16.66 29.7329 22.6547 29.2146 25.4213C28.9953 26.5921 28.5635 26.9846 28.1455 27.023C27.2371 27.1065 26.5472 26.4235 25.6673 25.8475C24.2904 24.9462 23.5126 24.3851 22.1761 23.5056C20.6316 22.4892 21.6329 21.9305 22.5131 21.0176C22.7434 20.7786 26.7461 17.1429 26.8236 16.813C26.8333 16.7718 26.8423 16.618 26.7508 16.5369C26.6594 16.4557 26.5243 16.4834 26.4269 16.5055C26.2888 16.5368 24.0893 17.9886 19.8283 20.861C19.204 21.2891 18.6385 21.4977 18.1318 21.4868C17.5732 21.4747 16.4988 21.1713 15.7 20.9121C14.7203 20.5941 13.9417 20.4259 14.0095 19.8858C14.0448 19.6045 14.4327 19.3168 15.1733 19.0227Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_79_274">
|
|
||||||
<rect width="17" height="14.06" fill="white" transform="translate(14 12.97)"/>
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip1_79_274">
|
|
||||||
<rect width="17" height="14.06" fill="white" transform="translate(14 12.97)"/>
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip2_79_274">
|
|
||||||
<rect width="16.9892" height="14.06" fill="white" transform="translate(14.0054 12.97)"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,21 +0,0 @@
|
||||||
<svg width="45" height="40" viewBox="0 0 45 40" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="45" height="40" rx="8" fill-opacity="0.7"/>
|
|
||||||
<g clip-path="url(#clip0_79_269)">
|
|
||||||
<g clip-path="url(#clip1_79_269)">
|
|
||||||
<g clip-path="url(#clip2_79_269)">
|
|
||||||
<path d="M24.0203 28.4289C24.0203 28.4289 24.5798 28.3692 24.8663 28.0733C25.1286 27.8022 25.1195 27.2907 25.1195 27.2907C25.1195 27.2907 25.0847 24.9019 26.2385 24.5492C27.3757 24.2024 28.8358 26.8593 30.3853 27.8809C31.5559 28.6534 32.4444 28.4843 32.4444 28.4843L36.5851 28.4289C36.5851 28.4289 38.7503 28.3007 37.7238 26.664C37.6389 26.5299 37.1249 25.4528 34.6459 23.2404C32.0486 20.9246 32.3974 21.2991 35.5238 17.2926C37.4281 14.8529 38.1893 13.3633 37.9512 12.7264C37.7253 12.1172 36.3243 12.279 36.3243 12.279L31.6635 12.3067C31.6635 12.3067 31.3178 12.2615 31.0616 12.4087C30.8114 12.553 30.6492 12.8897 30.6492 12.8897C30.6492 12.8897 29.9123 14.7771 28.9283 16.3832C26.8526 19.7703 26.0232 19.9495 25.6836 19.7397C24.8936 19.2485 25.0907 17.7692 25.0907 16.7184C25.0907 13.4348 25.6093 12.0662 24.0825 11.7121C23.576 11.594 23.203 11.5168 21.9067 11.5036C20.2434 11.4876 18.8364 11.5095 18.0388 11.884C17.5082 12.1333 17.0988 12.69 17.349 12.7221C17.6567 12.7614 18.3542 12.9028 18.7242 13.3867C19.2018 14.0119 19.1851 15.414 19.1851 15.414C19.1851 15.414 19.4595 19.2791 18.5437 19.7586C17.916 20.088 17.0548 19.4161 15.2035 16.3424C14.2559 14.7683 13.5402 13.0281 13.5402 13.0281C13.5402 13.0281 13.4023 12.7031 13.1551 12.5282C12.8564 12.3169 12.4395 12.2513 12.4395 12.2513L8.01061 12.279C8.01061 12.279 7.345 12.2965 7.10089 12.5749C6.88407 12.8212 7.08421 13.3327 7.08421 13.3327C7.08421 13.3327 10.5518 21.1315 14.4788 25.0622C18.0798 28.6651 22.1675 28.4289 22.1675 28.4289H24.0203Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_79_269">
|
|
||||||
<rect width="31" height="17" transform="translate(7 11.5)"/>
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip1_79_269">
|
|
||||||
<rect width="31" height="17" transform="translate(7 11.5)"/>
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="clip2_79_269">
|
|
||||||
<rect width="31" height="17" transform="translate(7 11.5)"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.0 KiB |
|
@ -3,28 +3,18 @@ import {Button, Checkbox} from "@nextui-org/react";
|
||||||
import {ChevronRightIcon} from "@nextui-org/shared-icons";
|
import {ChevronRightIcon} from "@nextui-org/shared-icons";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import LocalAPI from "@/service/localAPI";
|
import LocalAPI from "@/service/localAPI";
|
||||||
import {useState} from "react";
|
|
||||||
import _ from 'lodash';
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
type OrderInfoProps = {
|
type OrderInfoProps = {
|
||||||
setIsDisabled: (value: boolean) => void
|
setIsDisabled: (value: boolean) => void
|
||||||
isDisabled: boolean
|
isDisabled: boolean
|
||||||
}
|
}
|
||||||
const OrderInfo = (props: OrderInfoProps) => {
|
const OrderInfo = (props: OrderInfoProps) => {
|
||||||
|
|
||||||
const [coupon, setCoupon] = useState("");
|
|
||||||
const [couponApplied, setCouponApplied] = useState(false)
|
|
||||||
|
|
||||||
|
|
||||||
const totalProductPriceQs = useQuery(
|
const totalProductPriceQs = useQuery(
|
||||||
{
|
{
|
||||||
queryKey: ['totalProductPrice', couponApplied, coupon], queryFn: async () => {
|
queryKey: ['totalProductPrice'], queryFn: async () => {
|
||||||
const service = new LocalAPI()
|
const service = new LocalAPI()
|
||||||
if (couponApplied) {
|
return await service.totalProductPrice()
|
||||||
return await service.totalProductPrice(coupon)
|
|
||||||
}
|
|
||||||
return await service.totalProductPrice(undefined)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -33,38 +23,39 @@ const OrderInfo = (props: OrderInfoProps) => {
|
||||||
<h2 className={"text-subtitle-2 font-bold mb-2"}>Информация о заказе</h2>
|
<h2 className={"text-subtitle-2 font-bold mb-2"}>Информация о заказе</h2>
|
||||||
<div className="flex justify-between mb-2">
|
<div className="flex justify-between mb-2">
|
||||||
<span>Товаров на:</span>
|
<span>Товаров на:</span>
|
||||||
{/* TODO round value */}
|
<span>{String(totalProductPriceQs.data?.total_product_price).replace(/\B(?=(\d{3})+(?!\d))/g, " ")} ₽</span>
|
||||||
{/*<span>{String(totalProductPriceQs.data ? _.sum(totalProductPriceQs.data.data!.items.map(item => item.cart.product.price.BASE * item.cart.quantity)) : 0).replace(/\B(?=(\d{3})+(?!\d))/g, " ")} ₽</span>*/}
|
|
||||||
</div>
|
</div>
|
||||||
<form className="flex justify-between h-[50px] mb-2">
|
<form className="flex justify-between h-[50px] mb-2">
|
||||||
<Input isDisabled={totalProductPriceQs.data && couponApplied && _.some(totalProductPriceQs.data.data!.items, "discount")} isInvalid={totalProductPriceQs.data && couponApplied && _.every(totalProductPriceQs.data.data!.items, ["discount", null])} type="text" label="Введите промокод" variant={"bordered"}
|
<Input type="text" label="Введите промокод" variant={"bordered"}
|
||||||
className={"border-[#1E1E1E] mr-4"} onChange={(e) => setCoupon(e.target.value)}/>
|
className={"border-[#1E1E1E] mr-4"}/>
|
||||||
<Button isDisabled={couponApplied} className={"flex-auto h-full"}
|
<Button className={"flex-auto h-full"}
|
||||||
color={'primary'} onClick={() => setCouponApplied(true)}><ChevronRightIcon/></Button>
|
color={'primary'}><ChevronRightIcon/></Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{/*TODO calculate discount*/}
|
||||||
|
|
||||||
<div className="flex justify-between mb-2 border-b-1 border-b-black-1 pb-4">
|
<div className="flex justify-between mb-2 border-b-1 border-b-black-1 pb-4">
|
||||||
<span>Скидка: </span>
|
<span>Скидка: </span>
|
||||||
{/*<span>{totalProductPriceQs.data ? _.sum(totalProductPriceQs.data.data!.items.map(item => item.cart.product.price.BASE * item.cart.quantity)) - totalProductPriceQs.data.data!.total : 0} ₽</span>*/}
|
<span>0 ₽</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*TODO calculate discount + shipment*/}
|
{/*TODO calculate discount + shipment*/}
|
||||||
|
|
||||||
<div className="flex justify-between mb-2 items-center">
|
<div className="flex justify-between mb-2 items-center">
|
||||||
<span>Итого: </span>
|
<span>Итого: </span>
|
||||||
<span className={"text-title-3 font-bold"}>{String(totalProductPriceQs.data ? totalProductPriceQs.data.data!.total : 0).replace(/\B(?=(\d{3})+(?!\d))/g, " ")} ₽</span>
|
<span className={"text-title-3 font-bold"}>{String(totalProductPriceQs.data?.total_product_price).replace(/\B(?=(\d{3})+(?!\d))/g, " ")} ₽</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between mb-8 text-[#808080] text-subtitle-5">
|
<div className="flex justify-between mb-8 text-[#808080] text-subtitle-5">
|
||||||
<span>Сумма НДС: </span>
|
<span>Сумма НДС: </span>
|
||||||
<span>{totalProductPriceQs.data ? totalProductPriceQs.data.data!.total * 20 / 120 : 0} ₽</span>
|
<span>{totalProductPriceQs.data?.total_product_price * 0.2} ₽</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Checkbox isSelected={!props.isDisabled} onChange={() => {
|
<Checkbox isSelected={!props.isDisabled} onChange={() => {
|
||||||
props.setIsDisabled(!props.isDisabled)
|
props.setIsDisabled(!props.isDisabled)
|
||||||
}}
|
}}
|
||||||
className={"[&_span:last-child]:!text-[#808080] [&_span:last-child]:text-subtitle-5 [&_span:last-child]:leading-normal mb-6"}>Нажимая
|
className={"[&_span:last-child]:!text-[#808080] [&_span:last-child]:text-subtitle-5 [&_span:last-child]:leading-normal mb-6"}>Нажимая
|
||||||
кнопку «Оформить заказ», я даю согласие на <Link className={"text-primary"} href={"https://tehnohimgrupp.ru/upload/Согласие на ОПД Технохим.pdf"}>обработку моих персональных данных</Link></Checkbox>
|
кнопку «Оформить заказ», я даю согласие на обработку моих персональных данных</Checkbox>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
import {Autocomplete, AutocompleteItem, cn} from "@nextui-org/react";
|
|
||||||
import {useDebounce} from "@uidotdev/usehooks";
|
|
||||||
import localAPI from "@/service/localAPI";
|
|
||||||
import {Controller} from "react-hook-form";
|
|
||||||
import {InputPropsType} from "@/components/pages/order/types";
|
|
||||||
import {useQuery} from "@tanstack/react-query";
|
|
||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
const AddressInput = (props: InputPropsType) => {
|
|
||||||
const address = props.watch("address")
|
|
||||||
const debouncedAddress = useDebounce(address, 500)
|
|
||||||
|
|
||||||
const addressInfo = useQuery({
|
|
||||||
queryKey: ["address_info", debouncedAddress],
|
|
||||||
queryFn: async () => {
|
|
||||||
const service = new localAPI()
|
|
||||||
return await service.fetchAddresses(debouncedAddress)
|
|
||||||
},
|
|
||||||
staleTime: 1000
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Controller render={ ({field}) =>
|
|
||||||
<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]"),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
inputValue={field.value}
|
|
||||||
isLoading={addressInfo.isLoading}
|
|
||||||
onInputChange={(value) => {
|
|
||||||
props.setValue("address", value)
|
|
||||||
const obj = _.find(addressInfo.data, (p1) => p1.metaDataProperty.GeocoderMetaData.Address.formatted === value)
|
|
||||||
if (typeof obj !== "undefined") {
|
|
||||||
props.setValue("lat", +(obj.Point.pos.split(' ')[1]))
|
|
||||||
props.setValue("lon", +( obj.Point.pos.split(' ')[0]))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
items={addressInfo.data || []}
|
|
||||||
>
|
|
||||||
{ item => (
|
|
||||||
<AutocompleteItem key={item.metaDataProperty.GeocoderMetaData.Address.formatted} value={item.metaDataProperty.GeocoderMetaData.Address.formatted} classNames={{
|
|
||||||
title: cn("text-[#8F8F8F] data-[selected=true]:text-[#151515]"),
|
|
||||||
}}>
|
|
||||||
{item.metaDataProperty.GeocoderMetaData.Address.formatted}
|
|
||||||
</AutocompleteItem>
|
|
||||||
)}
|
|
||||||
</Autocomplete>
|
|
||||||
} name={"address"} control={props.control} rules={{required: "Поле обязательно для заполнения"}} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default AddressInput
|
|
|
@ -1,165 +0,0 @@
|
||||||
import {Dispatch, SetStateAction, useEffect, useRef, useState} from "react"
|
|
||||||
import Script from "next/script";
|
|
||||||
import {YMap, type YMapLocationRequest} from 'ymaps3';
|
|
||||||
import {Spinner, Tooltip} from "@nextui-org/react";
|
|
||||||
import {InputPropsType} from "@/components/pages/order/types";
|
|
||||||
import {useQuery} from "@tanstack/react-query";
|
|
||||||
import LocalAPI from "@/service/localAPI";
|
|
||||||
import {SdekPoint} from "@/service/types/local";
|
|
||||||
|
|
||||||
|
|
||||||
type GeoObject = {
|
|
||||||
lat: number,
|
|
||||||
lon: number,
|
|
||||||
name: string,
|
|
||||||
description: string
|
|
||||||
pvzId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SdekMapProps = {
|
|
||||||
objects: GeoObject[],
|
|
||||||
center: [number, number],
|
|
||||||
zoom?: number,
|
|
||||||
} & Omit<InputPropsType, 'control' | 'errors'>
|
|
||||||
|
|
||||||
const initMap = async (ref: React.RefObject<HTMLDivElement>,
|
|
||||||
setMapIsLoaded: Dispatch<SetStateAction<boolean>>,
|
|
||||||
setMap: Dispatch<SetStateAction<YMap | null>>
|
|
||||||
) => {
|
|
||||||
|
|
||||||
await ymaps3.ready;
|
|
||||||
setMapIsLoaded(true)
|
|
||||||
|
|
||||||
const LOCATION: YMapLocationRequest = {
|
|
||||||
center: [37.623082, 55.75254],
|
|
||||||
zoom: 9
|
|
||||||
};
|
|
||||||
|
|
||||||
const {YMap, YMapDefaultSchemeLayer, YMapMarker} = ymaps3;
|
|
||||||
if (!ref.current) return
|
|
||||||
|
|
||||||
const map = new YMap(ref.current, {location: LOCATION});
|
|
||||||
|
|
||||||
map.addChild(new YMapDefaultSchemeLayer({}))
|
|
||||||
.addChild(new ymaps3.YMapDefaultFeaturesLayer({zIndex: 1800}))
|
|
||||||
setMap(map)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const SdekElem = ({map, object, watch, setValue}: { map: YMap | null, object: SdekPoint } & Omit<InputPropsType, 'control' | 'errors'>) => {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const selectedPvzId = watch("pvzId")
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!map || !ref.current) return
|
|
||||||
const marker = new ymaps3.YMapMarker({
|
|
||||||
coordinates: [object.location.longitude, object.location.latitude],
|
|
||||||
draggable: false,
|
|
||||||
}, ref.current)
|
|
||||||
|
|
||||||
map.addChild(marker)
|
|
||||||
|
|
||||||
}, [map, ref.current]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip placement={"bottom-end"} content={
|
|
||||||
<div className="px-1 py-2 text-black">
|
|
||||||
<div className="text-small font-bold">{object.name}</div>
|
|
||||||
<div className="text-tiny">{object.note}</div>
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<div ref={ref} className={"relative h-[1px] w-[1px]"} onClick={() => setValue("pvzId", object.code)}>
|
|
||||||
<CDEKLogo isSelected={object.code === selectedPvzId}/>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CDEKLogo = ({isSelected}: { isSelected: boolean }) => (
|
|
||||||
<svg className={"absolute top-[-85px] left-[-30px]"} width="76" height="85" viewBox="0 0 76 85" fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M38.2221 85L12.0489 45.3333L64.3953 45.3333L38.2221 85Z" fill="#D9D9D9"/>
|
|
||||||
<g filter="url(#filter0_d_1020_18429)">
|
|
||||||
<circle cx="38.2222" cy="30.2222" r="30.2222" className={isSelected ? "fill-primary" : "fill-white"}/>
|
|
||||||
</g>
|
|
||||||
<g clip-path="url(#clip0_1020_18429)">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
||||||
d="M22.6584 34.0822H20.7036C17.6541 34.0822 19.8696 27.6053 22.4369 27.6053H25.5255C26.0207 27.6053 26.8939 27.6966 27.3239 26.4715L27.9885 24.5688H23.7531C21.4595 24.5688 19.6741 25.3768 18.3839 26.7322C16.1554 29.0519 15.3996 32.6748 15.9339 34.408C16.4422 36.011 17.8236 37.0666 19.9869 37.0927L21.668 37.1057H23.7401L24.2484 35.5809C24.6263 34.4993 23.7792 34.0822 22.6584 34.0822ZM46.455 31.3976L47.0934 29.3255H40.2778C39.144 29.3255 38.6357 29.6383 38.4403 30.2899L37.8017 32.362H44.6174C45.7512 32.362 46.2595 32.0492 46.455 31.3976ZM36.7852 35.0466L36.1466 37.1187H42.9624C44.0831 37.1187 44.6044 36.8059 44.7999 36.1543L45.4385 34.0822H38.6227C37.5019 34.0822 36.9937 34.395 36.7852 35.0466ZM48.0448 26.654L48.6836 24.5819H41.8677C40.7339 24.5819 40.2256 24.8946 40.0302 25.5463L39.3916 27.6183H46.2074C47.3282 27.6183 47.8363 27.3056 48.0448 26.654ZM37.7496 27.071C37.4628 25.1423 36.4333 24.5819 33.9311 24.5819H29.37L26.7114 32.362H28.3925C29.396 32.362 29.9043 32.375 30.4386 30.9415L31.5463 27.6053H33.2405C34.687 27.6053 34.3612 29.4168 33.6314 31.1761C32.9798 32.7269 31.846 34.0953 30.4777 34.0953H27.6497C26.5159 34.0953 25.9947 34.408 25.7861 35.0597L25.0824 37.1317H27.1545L29.1875 37.1187C30.9859 37.1057 32.4585 36.9753 34.1788 35.4245C36.0033 33.7695 38.1144 29.521 37.7496 27.071ZM60.66 24.5688H56.7113L52.9971 28.5176C52.5669 28.9737 52.1239 29.4298 51.6937 29.9641H51.6546L53.5051 24.5688H50.2865L45.9467 37.1187H49.1657L50.5469 33.17L51.9804 31.958L53.1143 35.5288C53.4661 36.6365 53.8312 37.1187 54.6132 37.1187H57.0762L54.5478 30.1075L60.66 24.5688Z"
|
|
||||||
fill="#1AB248"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<filter id="filter0_d_1020_18429" x="0.444445" y="0" width="75.5554" height="75.5554"
|
|
||||||
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
|
||||||
result="hardAlpha"/>
|
|
||||||
<feOffset dy="7.55556"/>
|
|
||||||
<feGaussianBlur stdDeviation="3.77778"/>
|
|
||||||
<feComposite in2="hardAlpha" operator="out"/>
|
|
||||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
|
||||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1020_18429"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1020_18429" result="shape"/>
|
|
||||||
</filter>
|
|
||||||
<clipPath id="clip0_1020_18429">
|
|
||||||
<rect width="45.3333" height="12.8184" fill="white" transform="translate(15.5557 24.5557)"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
const CdekMap = (props: SdekMapProps) => {
|
|
||||||
const ref = useRef(null);
|
|
||||||
const [mapIsLoaded, setMapIsLoaded] = useState<boolean>(false)
|
|
||||||
const [mapInstance, setMapInstance] = useState<YMap | null>(null)
|
|
||||||
|
|
||||||
const lat = props.watch("lat")
|
|
||||||
const lon = props.watch("lon")
|
|
||||||
|
|
||||||
const sdekPoints = useQuery({
|
|
||||||
queryKey: ["sdek-points", lat, lon],
|
|
||||||
queryFn: async () => {
|
|
||||||
const service = new LocalAPI()
|
|
||||||
if (!lat || !lon){ return []}
|
|
||||||
|
|
||||||
return await service.fetchSdekPoints(lat, lon)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!mapInstance || !mapIsLoaded) return
|
|
||||||
if (lat && lon) {
|
|
||||||
const LOCATION: YMapLocationRequest = {
|
|
||||||
center: [lon, lat],
|
|
||||||
zoom: 15
|
|
||||||
};
|
|
||||||
mapInstance.setLocation(LOCATION)
|
|
||||||
}
|
|
||||||
|
|
||||||
}, [mapInstance, mapIsLoaded, lat, lon]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Script src={"https://api-maps.yandex.ru/v3/?apikey=cc184d53-fc5f-4821-aa68-a767beea55d6&lang=ru_RU"}
|
|
||||||
onLoad={() => initMap(ref, setMapIsLoaded, setMapInstance)}/>
|
|
||||||
|
|
||||||
{
|
|
||||||
sdekPoints.isLoading && <Spinner />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
sdekPoints.data && sdekPoints.data.map((obj, idx) => <SdekElem key={idx} map={mapInstance} object={obj} {...props}/>)
|
|
||||||
}
|
|
||||||
|
|
||||||
<div ref={ref} className={"h-[600px] w-full"}>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CdekMap
|
|
|
@ -1,65 +0,0 @@
|
||||||
import {Radio, RadioProps} from "@nextui-org/radio";
|
|
||||||
import {cn} from "@nextui-org/react";
|
|
||||||
import StorageIcon from "../../../../public/storage.svg";
|
|
||||||
import DeliveryIcon from "../../../../public/delivery.svg";
|
|
||||||
import {Controller} from "react-hook-form";
|
|
||||||
import {InputPropsType} from "@/components/pages/order/types";
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<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")
|
|
||||||
}} {...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>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const DeliveryInput = ({control, deliveryType}: InputPropsType & { deliveryType: "take-away" | "delivery" }) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Controller control={control} name={"receivingMethod"} render={({field}) =>
|
|
||||||
<DeliveryTypeInput deliveryType={deliveryType} {...field} />}/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default DeliveryInput
|
|
|
@ -1,34 +0,0 @@
|
||||||
import {Input} from "@nextui-org/input";
|
|
||||||
import {Controller} from "react-hook-form";
|
|
||||||
import {InputPropsType} from "@/components/pages/order/types";
|
|
||||||
|
|
||||||
const EmailInput = ({control, errors} : InputPropsType) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EmailInput;
|
|
|
@ -1,25 +0,0 @@
|
||||||
import {Controller} from "react-hook-form";
|
|
||||||
import {Input} from "@nextui-org/input";
|
|
||||||
import {InputPropsType} from "@/components/pages/order/types";
|
|
||||||
|
|
||||||
const FullNameInput = (props: InputPropsType) => {
|
|
||||||
return (
|
|
||||||
<Controller name={"fullName"} control={props.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={!!props.errors.fullName}
|
|
||||||
errorMessage={props.errors.fullName && props.errors.fullName.message}
|
|
||||||
labelPlacement={"outside"} {...field}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FullNameInput
|
|
|
@ -1,80 +0,0 @@
|
||||||
import {Control, Controller, FieldErrors, UseFormSetValue, UseFormWatch} from "react-hook-form";
|
|
||||||
import {Input} from "@nextui-org/input";
|
|
||||||
import {FormDataValuesType, InputPropsType} from "@/components/pages/order/types";
|
|
||||||
import {useEffect, useState} from "react";
|
|
||||||
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (currentValue === "8") {
|
|
||||||
return `+7`;
|
|
||||||
}
|
|
||||||
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 ""
|
|
||||||
};
|
|
||||||
|
|
||||||
const PhoneInput = ({control, errors, watch, setValue} : InputPropsType) => {
|
|
||||||
|
|
||||||
const [phoneNumberPrev, setPhoneNumberPrev] = useState("")
|
|
||||||
const phoneNumberCur = watch("phoneNumber")
|
|
||||||
|
|
||||||
// const formRef = useRef<HTMLFormElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (phoneNumberCur === phoneNumberPrev) return
|
|
||||||
setValue('phoneNumber', normalizeInput(phoneNumberCur, phoneNumberPrev))
|
|
||||||
setPhoneNumberPrev(phoneNumberCur)
|
|
||||||
}, [phoneNumberCur])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
}/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PhoneInput;
|
|
|
@ -1,18 +0,0 @@
|
||||||
import {Control, FieldErrors, UseFormSetValue, UseFormWatch} from "react-hook-form";
|
|
||||||
|
|
||||||
export type FormDataValuesType = {
|
|
||||||
fullName: string
|
|
||||||
phoneNumber: string
|
|
||||||
email: string
|
|
||||||
receivingMethod: string
|
|
||||||
address?: string
|
|
||||||
deliveryTypeId?: number
|
|
||||||
paymentTypeId: number
|
|
||||||
comment: string,
|
|
||||||
pvzId?: string
|
|
||||||
lat?: number,
|
|
||||||
lon?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export type InputPropsType = { errors: FieldErrors<FormDataValuesType>, control: Control<FormDataValuesType>, watch: UseFormWatch<FormDataValuesType>, setValue: UseFormSetValue<FormDataValuesType> }
|
|
|
@ -1,23 +0,0 @@
|
||||||
import {Radio, RadioGroup} from "@nextui-org/radio";
|
|
||||||
|
|
||||||
const Step = ({value, title} : {value: string, title: string}) => {
|
|
||||||
return (
|
|
||||||
<Radio value={value}>
|
|
||||||
<span>{title}</span>
|
|
||||||
</Radio>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CallbackForm = ({steps} : {steps: {title: string, value: string}[]}) => {
|
|
||||||
return (
|
|
||||||
<form>
|
|
||||||
{steps.map((step, index) => (
|
|
||||||
<RadioGroup key={index} name="callback" defaultValue={step.value}>
|
|
||||||
<Step {...step}/>
|
|
||||||
</RadioGroup>
|
|
||||||
))}
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CallbackForm
|
|
|
@ -1,69 +0,0 @@
|
||||||
import {Divider} from "@nextui-org/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import Logo from "../../../public/header_logo.svg";
|
|
||||||
import TgIcon from "../../../public/tg_icon.svg"
|
|
||||||
import VkIcon from "../../../public/vk_icon.svg"
|
|
||||||
import YouTubeIcon from "../../../public/YouTubeIcon.svg"
|
|
||||||
import {Img} from "react-image";
|
|
||||||
|
|
||||||
const Footer = () => {
|
|
||||||
return (
|
|
||||||
<footer className={"bg-black-2 pt-4"}>
|
|
||||||
<div className="wrapper text-white grid grid-cols-12">
|
|
||||||
<div className="logo col-span-12 xl:col-span-3">
|
|
||||||
<Link href={"/"} className={"transition-none pt-8 pb-2 block"}>
|
|
||||||
<Logo/>
|
|
||||||
</Link>
|
|
||||||
<span className={"block mb-2 text-gray-3"}>Московская область, г. Домодедово,
|
|
||||||
Каширское ш, 4, к.1, оф.330</span>
|
|
||||||
<a className={"block"} href={"tel:+74951919720"}>+7(495)191-97-20</a>
|
|
||||||
|
|
||||||
<div className="socials my-4 flex [&_a]:mr-2">
|
|
||||||
<a href="https://vk.com/relynolli_vk" className={"group"}><VkIcon className={"fill-gray-3 group-hover:fill-primary transition-colors [&_path]:fill-white"}/></a>
|
|
||||||
<a href="https://t.me/relynolli" className={"group"}><TgIcon className={"fill-gray-3 group-hover:fill-primary transition-colors [&_path]:fill-white"} /></a>
|
|
||||||
<a href="https://t.me/relynolli" className={"group"}><YouTubeIcon className={"fill-gray-3 group-hover:fill-primary transition-colors [&_path]:fill-white last:[&_path]:fill-gray-3"} /></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*<div className={"markets my-2"}>*/}
|
|
||||||
{/* <a href="#" className={"group"}><Img src={'/ozon_icon.png'} className={"w-[40px] h-[40px] grayscale group-hover:grayscale-0 transition-all"} /></a>*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 xl:col-span-3 xl:col-start-5 mt-5">
|
|
||||||
<h2 className={"text-2xl hover:text-primary transition-colors"}>Бренд</h2>
|
|
||||||
<ul className={"text-gray-3 text-sm font-semibold my-2"}>
|
|
||||||
<li className={"hover:text-primary transition-colors"}>Персонализация</li>
|
|
||||||
<li className={"hover:text-primary transition-colors"}>Технологии</li>
|
|
||||||
<li className={"hover:text-primary transition-colors"}>Производство</li>
|
|
||||||
<li className={"hover:text-primary transition-colors"}><Link href={"/news"}>Новости</Link></li>
|
|
||||||
<li className={"hover:text-primary transition-colors"}>Карьера</li>
|
|
||||||
<li className={"hover:text-primary transition-colors"}>Миссия</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 xl:col-span-3 mt-5">
|
|
||||||
<h2 className={"text-2xl hover:text-primary transition-colors"}><Link href={"/catalog"}>Продукция</Link></h2>
|
|
||||||
<ul className={"text-gray-3 text-sm font-semibold my-2"}>
|
|
||||||
<li className={"hover:text-primary transition-colors"}><Link href={"/catalog"}>Relynolli ® Standart M</Link></li>
|
|
||||||
<li className={"hover:text-primary transition-colors"}><Link href={"/catalog"}>Relynolli ® Premium M</Link></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 xl:col-span-2 mt-5">
|
|
||||||
<h2 className={"text-2xl hover:text-primary transition-colors"}>Информация</h2>
|
|
||||||
<ul className={"text-gray-3 text-sm font-semibold my-2"}>
|
|
||||||
<li className={"hover:text-primary transition-colors"}>Оплата</li>
|
|
||||||
<li className={"hover:text-primary transition-colors"}><Link href={"/contact"}>Контакты</Link></li>
|
|
||||||
<li className={"hover:text-primary transition-colors"}>Поддержка и рекламации</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<Divider className={"col-span-12 my-8 h-[1px] w-full bg-gray-3"}/>
|
|
||||||
<div className="col-span-12">
|
|
||||||
<h2>© ООО "ТД Технохим Групп" 2024</h2>
|
|
||||||
<p className={"text-gray-3 text-sm font-semibold my-2 mt-4"}><Link href={"https://tehnohimgrupp.ru/upload/Политика_обработки_ПДн_ТХГ.pdf"}>Политика конфиденциальности</Link></p>
|
|
||||||
<p className={"text-gray-3 text-sm font-semibold my-2"}><Link href={"https://tehnohimgrupp.ru/upload/Согласие на ОПД Технохим.pdf"}>Обработка персональных данных</Link></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</footer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Footer
|
|
|
@ -17,7 +17,6 @@ import {useQuery, useQueryClient} from "@tanstack/react-query";
|
||||||
import LocalAPI from "@/service/localAPI";
|
import LocalAPI from "@/service/localAPI";
|
||||||
import {HTMLProps, useEffect, useState} from "react";
|
import {HTMLProps, useEffect, useState} from "react";
|
||||||
import {tv} from "tailwind-variants"
|
import {tv} from "tailwind-variants"
|
||||||
import {Img} from "react-image";
|
|
||||||
|
|
||||||
const NavbarMenuToggle = ({isOpened, ...props}: { isOpened: boolean} & HTMLProps<HTMLDivElement>) => {
|
const NavbarMenuToggle = ({isOpened, ...props}: { isOpened: boolean} & HTMLProps<HTMLDivElement>) => {
|
||||||
|
|
||||||
|
@ -73,9 +72,9 @@ const NavbarMenuToggle = ({isOpened, ...props}: { isOpened: boolean} & HTMLProps
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{title: "Каталог", href: "/catalog"},
|
{title: "Каталог", href: "/catalog"},
|
||||||
{title: "Новости", href: "/news"},
|
{title: "Бренд", href: "#"},
|
||||||
{title: "Статьи", href: "/articles"},
|
{title: "Статьи", href: "#"},
|
||||||
{title: "Контакты", href: "/contact"},
|
{title: "Контакты", href: "#"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -92,8 +91,8 @@ const Header = () => {
|
||||||
return (
|
return (
|
||||||
<header className={"w-full top-0 left-0 bg-black-4 py-[33px] z-50 h-[100px] relative"}>
|
<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 ">
|
<div className="wrapper flex justify-between items-center ">
|
||||||
<Link href={"/"} className={"transition-none hover:opacity-100 w-3/4 xl:w-1/4"} onClick={() => setIsOpened(false)}>
|
<Link href={"/"} className={"transition-none hover:opacity-100 w-1/4"}>
|
||||||
<Img src={'/header_logo.svg'} className={"w-full max-w-[271px]"} />
|
<Logo/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className={"xl:flex justify-between items-center w-[30%] hidden"}>
|
<nav className={"xl:flex justify-between items-center w-[30%] hidden"}>
|
||||||
|
@ -137,7 +136,7 @@ const Header = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cart.data &&
|
{cart.data &&
|
||||||
<Badge isInvisible={!Boolean(cart.data.data)} color={"primary"} content={cart.data.data && cart.data.data.length}
|
<Badge isInvisible={cart.data?.length === 0} color={"primary"} content={cart.data?.length}
|
||||||
className={"text-black font-semibold"}>
|
className={"text-black font-semibold"}>
|
||||||
<Link href={'/cart'}
|
<Link href={'/cart'}
|
||||||
className={"rounded-[8px] group transition-colors cursor-pointer bg-transparent hover:bg-primary flex items-center px-3 h-[50px]"}>
|
className={"rounded-[8px] group transition-colors cursor-pointer bg-transparent hover:bg-primary flex items-center px-3 h-[50px]"}>
|
||||||
|
@ -167,7 +166,7 @@ const Header = () => {
|
||||||
menuItems.map(item => (
|
menuItems.map(item => (
|
||||||
<Link href={item.href}
|
<Link href={item.href}
|
||||||
className={"text-2xl text-white hover:text-green-2 hover:opacity-100 transition-colors"}
|
className={"text-2xl text-white hover:text-green-2 hover:opacity-100 transition-colors"}
|
||||||
key={item.title} onClick={() => setIsOpened(false)}>{item.title}</Link>
|
key={item.title}>{item.title}</Link>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -199,18 +198,23 @@ const Header = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cart.data &&
|
{cart.data &&
|
||||||
<Badge isInvisible={!Boolean(cart.data.data)} color={"primary"} content={cart.data.data && cart.data.data.length}
|
<Badge isInvisible={cart.data?.length === 0} color={"primary"} content={cart.data?.length}
|
||||||
className={"text-black font-semibold"}>
|
className={"text-black font-semibold"}>
|
||||||
<Link href={'/cart'}
|
<Link href={'/cart'}
|
||||||
onClick={() => setIsOpened(false)}
|
|
||||||
className={"rounded-[8px] group transition-colors cursor-pointer bg-transparent hover:bg-primary flex items-center px-3 h-[50px]"}>
|
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"}/>
|
<CartLogo className={"fill-white group-hover:fill-black-1 transition-colors"}/>
|
||||||
</Link>
|
</Link>
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import Header from "@/components/reusable/header";
|
import Header from "@/components/reusable/header";
|
||||||
import {Mulish} from "next/font/google";
|
import {Mulish} from "next/font/google";
|
||||||
import Footer from "@/components/reusable/footer";
|
|
||||||
import Script from "next/script";
|
|
||||||
|
|
||||||
const mulish = Mulish({
|
const mulish = Mulish({
|
||||||
subsets: ["cyrillic", "latin"],
|
subsets: ["cyrillic", "latin"],
|
||||||
|
@ -14,7 +12,6 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||||
<main className={`${mulish.variable} font-mulish`}>
|
<main className={`${mulish.variable} font-mulish`}>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
export default Layout
|
export default Layout
|
|
@ -1,56 +0,0 @@
|
||||||
import React, {useState} from "react";
|
|
||||||
|
|
||||||
export const Box = ({title, isCompleted}: {title: string, isCompleted: boolean}): JSX.Element => {
|
|
||||||
if (isCompleted) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-[66px] h-[66px]">
|
|
||||||
<div className="w-[68px] h-[66px] top-0 left-0 bg-primary rounded-[100%]">
|
|
||||||
<div className="w-[66px] h-[66px] rounded-[50px] flex items-center justify-center font-bold text-2xl ">
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="w-[66px] h-[66px]">
|
|
||||||
<div className="w-[68px] h-[66px] top-0 left-0 rounded-[100%] text-primary border-2 border-primary">
|
|
||||||
<div className="w-[66px] h-[66px] rounded-[50px] flex items-center justify-center font-bold text-2xl ">
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const Stepper = ({steps = []} : {steps: {title: string, isActive?: boolean}[]}) => {
|
|
||||||
const [lastActiveIdx, setLastActiveIdx] = useState(0)
|
|
||||||
|
|
||||||
const getIsCompleted = (idx: number, step: {title: string, isActive?: boolean}) => {
|
|
||||||
if (step.isActive && idx >= lastActiveIdx) {
|
|
||||||
// setLastActiveIdx(idx)
|
|
||||||
}
|
|
||||||
return idx <= lastActiveIdx
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={"stepper"}>
|
|
||||||
<div className="flex items-center gap-[7px] relative">
|
|
||||||
{steps.map((step, idx) => (
|
|
||||||
<>
|
|
||||||
<Box title={step.title} key={idx} isCompleted={getIsCompleted(idx, step)}/>
|
|
||||||
{idx < steps.length - 1 &&
|
|
||||||
<svg width="47" height="16" viewBox="0 0 47 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M46.7071 8.70711C47.0976 8.31658 47.0976 7.68342 46.7071 7.29289L40.3431 0.928932C39.9526 0.538408 39.3195 0.538408 38.9289 0.928932C38.5384 1.31946 38.5384 1.95262 38.9289 2.34315L44.5858 8L38.9289 13.6569C38.5384 14.0474 38.5384 14.6805 38.9289 15.0711C39.3195 15.4616 39.9526 15.4616 40.3431 15.0711L46.7071 8.70711ZM0 9H46V7H0V9Z" fill="#92E727"/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
export default Stepper;
|
|
|
@ -16,7 +16,7 @@ type WrapperProps = {
|
||||||
|
|
||||||
const Wrapper = (props: WrapperProps) => {
|
const Wrapper = (props: WrapperProps) => {
|
||||||
return <>
|
return <>
|
||||||
<section className={"bg-white text-black py-7"}>
|
<section className={"bg-white text-black pt-7"}>
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
{
|
{
|
||||||
props.breadcrumbs &&
|
props.breadcrumbs &&
|
||||||
|
@ -31,40 +31,7 @@ const Wrapper = (props: WrapperProps) => {
|
||||||
{
|
{
|
||||||
props.breadcrumbs.map(item =>
|
props.breadcrumbs.map(item =>
|
||||||
<BreadcrumbItem startContent={item.icon} key={item.name}>
|
<BreadcrumbItem startContent={item.icon} key={item.name}>
|
||||||
<Link className={"!whitespace-break-spaces"} href={item.link}>{item.name}</Link>
|
<Link href={item.link}>{item.name}</Link>
|
||||||
</BreadcrumbItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Breadcrumbs>
|
|
||||||
}
|
|
||||||
<h1 className={"mt-4 lg:text-7xl text-3xl font-bold italic uppercase pb-16"}>{props.title}</h1>
|
|
||||||
|
|
||||||
{props.children}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const WrapperDark =(props: WrapperProps) => {
|
|
||||||
return <>
|
|
||||||
<section className={"bg-black-2 text-white py-7"}>
|
|
||||||
<div className="wrapper">
|
|
||||||
{
|
|
||||||
props.breadcrumbs &&
|
|
||||||
<Breadcrumbs separator="/"
|
|
||||||
itemClasses={{
|
|
||||||
separator: "px-2 text-gray-3"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BreadcrumbItem startContent={<HomeIcon/>} className={"text-white"}>
|
|
||||||
<Link className={"text-gray-3"} href={"/"}>Главная</Link>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
{
|
|
||||||
props.breadcrumbs.map(item =>
|
|
||||||
<BreadcrumbItem startContent={item.icon} key={item.name}>
|
|
||||||
<Link className={"text-gray-3"} href={item.link}>{item.name}</Link>
|
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,13 @@
|
||||||
import {Html, Head, Main, NextScript} from "next/document";
|
import { Html, Head, Main, NextScript } from "next/document";
|
||||||
|
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
<Html lang="en">
|
<Html lang="en">
|
||||||
<Head/>
|
<Head />
|
||||||
<body>
|
<body>
|
||||||
<Main/>
|
<Main />
|
||||||
<NextScript/>
|
<NextScript />
|
||||||
|
</body>
|
||||||
<script
|
</Html>
|
||||||
dangerouslySetInnerHTML={{
|
);
|
||||||
__html: `
|
|
||||||
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
|
||||||
m[i].l=1*new Date();
|
|
||||||
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
|
||||||
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
|
|
||||||
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
|
|
||||||
|
|
||||||
ym(95565760, "init", {
|
|
||||||
clickmap:true,
|
|
||||||
trackLinks:true,
|
|
||||||
accurateTrackBounce:true,
|
|
||||||
webvisor:true
|
|
||||||
});
|
|
||||||
`,
|
|
||||||
}}/>
|
|
||||||
<noscript>
|
|
||||||
<div>
|
|
||||||
<img src="https://mc.yandex.ru/watch/12345678" style={{position: 'absolute', left: '-9999px'}}
|
|
||||||
alt=""/>
|
|
||||||
</div>
|
|
||||||
</noscript>
|
|
||||||
</body>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import LocalAPI from "@/service/localAPI";
|
|
||||||
import {InferGetStaticPropsType} from "next";
|
|
||||||
import {Img} from "react-image";
|
|
||||||
import Wrapper from "@/components/reusable/wrapper";
|
|
||||||
|
|
||||||
const News = (props: InferGetStaticPropsType<typeof getStaticProps>) => {
|
|
||||||
return (
|
|
||||||
<Wrapper title={props.news.name} breadcrumbs={[{name: "Новости", link: "/articles"}, {name: props.news.name, link: "/articles/" + props.news.code}]} >
|
|
||||||
{
|
|
||||||
props.news.picture && <Img className={"max-h-[250px] lg:max-h-[500px] mb-6 mx-auto rounded-[20px]"} src={"https://relynolli.ru/upload/" + props.news.picture} alt={props.news.name} />
|
|
||||||
}
|
|
||||||
<div className="content text-base lg:text-2xl [&>*]:mb-4 [&_img]:max-h-[250px] [&_img]:lg:max-h-[500px] [&_a]:text-primary break-words" dangerouslySetInnerHTML={{__html: props.news.content}}></div>
|
|
||||||
</Wrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default News
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const service = new LocalAPI()
|
|
||||||
const news = await service.fetchArticles()
|
|
||||||
return {
|
|
||||||
paths: news.data!.map(item => ({
|
|
||||||
params: {
|
|
||||||
slug: item.code
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
fallback: "blocking",
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStaticProps({params: {slug}}: { params: { slug: string } }) {
|
|
||||||
|
|
||||||
const service = new LocalAPI()
|
|
||||||
console.log(slug)
|
|
||||||
try {
|
|
||||||
const news = await service.retrieveArticle(slug)
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
news: news.data!
|
|
||||||
},
|
|
||||||
revalidate: 10 * 60
|
|
||||||
}}
|
|
||||||
catch (e) {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
revalidate: 10 * 60
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
import {Img} from "react-image";
|
|
||||||
import {useQuery} from "@tanstack/react-query";
|
|
||||||
import LocalAPI from "@/service/localAPI";
|
|
||||||
import {News} from "@/service/types/local";
|
|
||||||
import {WrapperDark} from "@/components/reusable/wrapper";
|
|
||||||
import {Skeleton} from "@nextui-org/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const NewsCard = (props: News) => {
|
|
||||||
return (
|
|
||||||
<Link href={"/articles/" + props.code}>
|
|
||||||
<div className="w-full max-w-[476px] relative group">
|
|
||||||
<div className="h-64 w-full overflow-hidden relative z-20">
|
|
||||||
<Img className="h-full w-full rounded-[20px] mb-5 object-cover"
|
|
||||||
src={"https://relynolli.ru/upload/" + props.picture}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-primary w-full opacity-0 group-hover:opacity-100 absolute top-1/2 left-0 z-10 bottom-0 -translate-y-10 group-hover:translate-y-0 transition-all rounded-b-[20px]"></div>
|
|
||||||
|
|
||||||
<div className="z-20 relative p-2">
|
|
||||||
<div className="w-full text-white group-hover:text-black-2 text-xl font-semibold transition-all">{props.name}</div>
|
|
||||||
<div className="text-neutral-400 text-base font-semibold group-hover:text-black-2 transition-all">{new Date(props.date).toLocaleDateString()}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const News = () => {
|
|
||||||
|
|
||||||
const queryNews = useQuery({
|
|
||||||
queryKey: ["articles"], queryFn: async () => {
|
|
||||||
const service = new LocalAPI()
|
|
||||||
return await service.fetchArticles()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WrapperDark title={"Статьи"} breadcrumbs={[{name: "Статьи", link: "/articles"}]}>
|
|
||||||
|
|
||||||
<div className="news-container grid grid-cols-1 lg:grid-cols-3 gap-5">
|
|
||||||
{
|
|
||||||
queryNews.data && queryNews.data.data!.map(news => <NewsCard key={news.id} {...news}/>)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
queryNews.isFetching && Array(10).fill(0).map((_, index) => <Skeleton key={index} className={"rounded-[20px] !bg-gray-3"}>
|
|
||||||
<div className="w-full h-64 "></div>
|
|
||||||
</Skeleton> )
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</WrapperDark>)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default News
|
|
|
@ -1,21 +1,23 @@
|
||||||
import {BreadcrumbItem, Breadcrumbs, Button, Checkbox} from "@nextui-org/react";
|
import {BreadcrumbItem, Breadcrumbs, Button, Checkbox} from "@nextui-org/react";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
import MinusIcon from "@/../public/minus_icon.svg"
|
import MinusIcon from "@/../public/minus_icon.svg"
|
||||||
import PlusIcon from "@/../public/plus_icon.svg"
|
import PlusIcon from "@/../public/plus_icon.svg"
|
||||||
import CrossIcon from "@/../public/cross.svg"
|
import CrossIcon from "@/../public/cross.svg"
|
||||||
import {ChevronRightIcon} from "@nextui-org/shared-icons";
|
import {ChevronRightIcon} from "@nextui-org/shared-icons";
|
||||||
|
import {toggleCart} from "@/store/cart"
|
||||||
|
import {ResponseData} from "@/pages/api/v1/catalog";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import Wrapper from "@/components/reusable/wrapper";
|
import Wrapper from "@/components/reusable/wrapper";
|
||||||
import OrderInfo from "@/components/pages/cart/orderInfo";
|
import OrderInfo from "@/components/pages/cart/orderInfo";
|
||||||
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
|
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
|
||||||
import LocalAPI from "@/service/localAPI";
|
import LocalAPI from "@/service/localAPI";
|
||||||
import {CartItem} from "@/service/types/local";
|
|
||||||
import {Img} from "react-image";
|
|
||||||
|
|
||||||
|
|
||||||
const CartCard = (item: CartItem) => {
|
const CartCard = (product: Partial<ResponseData> & { quantity: number, available_quantity: number }) => {
|
||||||
|
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
@ -34,39 +36,39 @@ const CartCard = (item: CartItem) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="card relative grid grid-cols-10 gap-4 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 mb-5">
|
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">
|
||||||
|
|
||||||
<Img className={"col-span-10 xl:col-span-3 max-w-[200px] max-h-[250px]"} src={`https://relynolli.ru/upload/${item.product.properties!.main_image![0]}`} alt={"oil"}/>
|
<Image className={"grow"} src={`https://tehnohimgrupp.ru/upload/${product.properties!.main_image![0]}`}
|
||||||
<div className="col-span-10 xl:col-span-3 text-block flex flex-col justify-between font-bold text-xl xl:text-base">
|
width={100} height={126} alt={"oil"}/>
|
||||||
<p>{item.product.name}</p>
|
<div className="text-block flex flex-col h-1/2 justify-between py-5 w-1/3">
|
||||||
<p className={"text-[#8F8F8F] font-normal pt-2"}>Артикул: {item.product.properties.vendor_code}</p>
|
<p>{product.name}</p>
|
||||||
|
<p className={"text-[#8F8F8F]"}>Артикул: {product.properties!.vendor_code}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className={"col-span-3 xl:col-span-1"}>{`${+(item.product.properties!.weight!) / 1000} кг`}</span>
|
|
||||||
|
|
||||||
<span className={"col-span-3 xl:col-span-1"}>{item.product.properties!.volume}</span>
|
<span className={"grow"}>{`${+(product.properties!.weight!) / 1000} кг`}</span>
|
||||||
|
|
||||||
<div className="flex col-start-8 col-span-3 xl:col-span-2 justify-between relative z-20">
|
<span className={"grow"}>{product.properties!.volume}</span>
|
||||||
<button onClick={() => changeQuantity.mutate({productId: item.product.id!, quantity: item.quantity - 1})
|
|
||||||
|
<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"}>
|
className={"bg-white flex drop-shadow w-[30px] h-[30px] rounded items-center justify-center hover:bg-primary transition-colors"}>
|
||||||
<MinusIcon/></button>
|
<MinusIcon/></button>
|
||||||
<span>{item.quantity}</span>
|
<span>{product.quantity}</span>
|
||||||
<button onClick={() => changeQuantity.mutate({productId: item.product.id!, quantity: item.quantity + 1})}
|
<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"}>
|
className={"bg-white flex drop-shadow w-[30px] h-[30px] rounded items-center justify-center hover:bg-primary transition-colors"}>
|
||||||
<PlusIcon/></button>
|
<PlusIcon/></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col font-bold text-right text-3xl col-span-10 border-t-1 border-t-primary pt-4">
|
<div className="flex flex-col font-bold grow text-center">
|
||||||
{item.product.price!.BASE} ₽
|
{product.price!.BASE} ₽
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className={"z-20 absolute top-8 right-0"} onClick={() => {
|
<button className={"relative z-20"} onClick={() => toggleCart(product as ResponseData)}>
|
||||||
changeQuantity.mutate({productId: item.product.id!, quantity: 0})
|
|
||||||
}}>
|
|
||||||
<CrossIcon className={"hover:fill-red-500 fill-[#8F8F8F] transition-colors"}/>
|
<CrossIcon className={"hover:fill-red-500 fill-[#8F8F8F] transition-colors"}/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link href={"/catalog/" + item.product.code} className={"absolute top-0 left-0 w-full h-full z-10"}></Link>
|
<Link href={"/catalog/" + product.code} className={"absolute top-0 left-0 w-full h-full z-10"}></Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -75,7 +77,7 @@ const PlaceHolder = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="card relative col-span-10 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">
|
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">
|
<div className="flex flex-col w-1/3 text-center py-16">
|
||||||
<span className={"pb-7"}>В корзине пока что ничего нет, выберите необходимые товары в каталоге</span>
|
<span className={"pb-7"}>В корзине пока что ничего нет, выберите необходимые товары в каталоге</span>
|
||||||
|
@ -91,7 +93,7 @@ const PlaceHolder = () => {
|
||||||
|
|
||||||
const Cart = () => {
|
const Cart = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isDisabled, setIsDisabled] = useState(false)
|
const [isDisabled, setIsDisabled] = useState(true)
|
||||||
|
|
||||||
|
|
||||||
const cart = useQuery({
|
const cart = useQuery({
|
||||||
|
@ -106,16 +108,17 @@ const Cart = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper title={"Корзина"} breadcrumbs={[{name: "Корзина", link: "/cart"}]}>
|
<Wrapper title={"Корзина"} breadcrumbs={[{name: "Корзина", link: "/cart"}]}>
|
||||||
|
|
||||||
<div className="grid grid-cols-10 pb-16 justify-between gap-5">
|
<div className="flex w-full pb-16 justify-between">
|
||||||
|
|
||||||
{(cart.data && cart.data.data) ? (
|
{
|
||||||
|
cart.data?.length ? (
|
||||||
<>
|
<>
|
||||||
<div className="cards order-2 col-span-10 xl:order-none xl:col-span-7 flex-col">
|
<div className="cards w-8/12 [&>*]:mb-7">
|
||||||
{cart.data.data.map(item => <CartCard key={item.id} {...item} />)}
|
{cart.data.map(item => <CartCard key={item.id} {...item}/>)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex order-1 col-span-10 xl:order-none xl:col-span-3 flex-col">
|
<div className="flex flex-col w-[30%]">
|
||||||
|
|
||||||
<OrderInfo
|
<OrderInfo
|
||||||
setIsDisabled={setIsDisabled} isDisabled={isDisabled}/>
|
setIsDisabled={setIsDisabled} isDisabled={isDisabled}/>
|
||||||
|
@ -127,6 +130,8 @@ const Cart = () => {
|
||||||
|
|
||||||
</>) : <PlaceHolder/>
|
</>) : <PlaceHolder/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {BreadcrumbItem, Breadcrumbs, Button, Skeleton, Spinner} from "@nextui-org/react";
|
import {BreadcrumbItem, Breadcrumbs, Button, Skeleton, Spinner} from "@nextui-org/react";
|
||||||
import HomeIcon from "../../../public/home_icon.svg";
|
import HomeIcon from "../../../public/home_icon.svg";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import axios from "axios";
|
||||||
|
import {ResponseData} from "@/pages/api/v1/catalog";
|
||||||
import {InferGetStaticPropsType} from "next";
|
import {InferGetStaticPropsType} from "next";
|
||||||
import Cart from "@/../public/cart.svg"
|
import Cart from "@/../public/cart.svg"
|
||||||
import Favourites from "@/../public/favourites_icon.svg"
|
import Favourites from "@/../public/favourites_icon.svg"
|
||||||
|
@ -10,11 +12,10 @@ import useClient from "@/hooks/useClient";
|
||||||
import LocalAPI from "@/service/localAPI";
|
import LocalAPI from "@/service/localAPI";
|
||||||
import {useQuery, useQueryClient, useMutation} from "@tanstack/react-query";
|
import {useQuery, useQueryClient, useMutation} from "@tanstack/react-query";
|
||||||
import {Img} from 'react-image'
|
import {Img} from 'react-image'
|
||||||
import {CartItem, Wrapper} from "@/service/types/local";
|
|
||||||
|
|
||||||
const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => {
|
const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => {
|
||||||
const {favourites} = useSnapshot(favouritesStore)
|
const {favourites} = useSnapshot(favouritesStore)
|
||||||
const cartItems = useQuery<Wrapper<CartItem[]>>({
|
const cartItems = useQuery<any[]>({
|
||||||
queryKey: ['cart']
|
queryKey: ['cart']
|
||||||
})
|
})
|
||||||
const isClient = useClient()
|
const isClient = useClient()
|
||||||
|
@ -24,7 +25,7 @@ const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => {
|
||||||
const toggleCart = useMutation({
|
const toggleCart = useMutation({
|
||||||
mutationFn: async ({productId, quantity}: { productId: number, quantity: number }) => {
|
mutationFn: async ({productId, quantity}: { productId: number, quantity: number }) => {
|
||||||
const service = new LocalAPI()
|
const service = new LocalAPI()
|
||||||
if (cartItems.data && cartItems.data.data && cartItems.data.data.find(item => item.product.id === productId)) {
|
if (cartItems.data!.find(({id}) => id === productId)) {
|
||||||
return await service.deleteCartItem(productId)
|
return await service.deleteCartItem(productId)
|
||||||
}
|
}
|
||||||
return await service.addCartItem(productId, quantity)
|
return await service.addCartItem(productId, quantity)
|
||||||
|
@ -53,7 +54,7 @@ const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => {
|
||||||
Каталог
|
Каталог
|
||||||
</Link>
|
</Link>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbItem><Link className={"!whitespace-break-spaces"} href={"/catalog/" + product.name}>{product.name}</Link></BreadcrumbItem>
|
<BreadcrumbItem>{product.name}</BreadcrumbItem>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
<div className="wrapper mt-12 grid grid-cols-1 lg:grid-cols-2 gap-4 items-center">
|
<div className="wrapper mt-12 grid grid-cols-1 lg:grid-cols-2 gap-4 items-center">
|
||||||
|
@ -71,8 +72,8 @@ const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => {
|
||||||
<span
|
<span
|
||||||
className={"text-base md:text-xl block mb-7 text-[#E0E3E3]"}>Тип: {product.properties.oil_type}</span>
|
className={"text-base md:text-xl block mb-7 text-[#E0E3E3]"}>Тип: {product.properties.oil_type}</span>
|
||||||
</div>
|
</div>
|
||||||
{/*<Button isDisabled color={"warning"} className={"text-black italic md:text-xl text-sm py-8 mb-7 w-full lg:w-auto"}><span*/}
|
<Button isDisabled color={"warning"} className={"text-black italic md:text-xl text-sm py-8 mb-7 w-full lg:w-auto"}><span
|
||||||
{/* className={"font-bold"}>Скидка 10%</span> при покупке более 12 шт</Button>*/}
|
className={"font-bold"}>Скидка 10%</span> при покупке более 12 шт</Button>
|
||||||
<div className="grid items-center grid-cols-2 lg:grid-cols-4 gap-5 pb-5">
|
<div className="grid items-center grid-cols-2 lg:grid-cols-4 gap-5 pb-5">
|
||||||
|
|
||||||
<span className="font-bold text-3xl text-white text-center">
|
<span className="font-bold text-3xl text-white text-center">
|
||||||
|
@ -84,7 +85,7 @@ const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => {
|
||||||
<Button onClick={() => toggleCart.mutate({productId: product.id, quantity: 1})} color={"primary"} className={"order-3 col-span-2 lg:order-none text-black font-extrabold uppercase italic h-[70px]"}
|
<Button onClick={() => toggleCart.mutate({productId: product.id, quantity: 1})} color={"primary"} className={"order-3 col-span-2 lg:order-none text-black font-extrabold uppercase italic h-[70px]"}
|
||||||
startContent={<Cart/>}>
|
startContent={<Cart/>}>
|
||||||
|
|
||||||
{(cartItems.data && cartItems.data.data && cartItems.data.data.map((item)=> item.product.id).includes(product.id)) ? "В корзине" : "Добавить в корзину"}
|
{cartItems.data && cartItems.data.map(({id})=> id).includes(product.id) ? "В корзине" : "Добавить в корзину"}
|
||||||
|
|
||||||
</Button>}
|
</Button>}
|
||||||
{
|
{
|
||||||
|
@ -180,8 +181,8 @@ export default OilCard
|
||||||
|
|
||||||
export const getStaticPaths = async () => {
|
export const getStaticPaths = async () => {
|
||||||
const service = new LocalAPI()
|
const service = new LocalAPI()
|
||||||
const {data} = await service.getCatalogItems({}, 1)
|
const data = await service.getCatalogItems() as {code: string}[]
|
||||||
const codes = data!.map(item => ({params: {code: item.code}}))
|
const codes = data.map(item => ({params: {code: item.code}}))
|
||||||
return {
|
return {
|
||||||
paths: [
|
paths: [
|
||||||
...codes
|
...codes
|
||||||
|
@ -193,20 +194,10 @@ export const getStaticPaths = async () => {
|
||||||
|
|
||||||
export const getStaticProps = async ({params: {code}}: { params: { code: string } }) => {
|
export const getStaticProps = async ({params: {code}}: { params: { code: string } }) => {
|
||||||
const service = new LocalAPI()
|
const service = new LocalAPI()
|
||||||
try {
|
const data = await service.getCatalogItemByCode(code)
|
||||||
const data = await service.getCatalogItemByCode(code)
|
return {
|
||||||
return {
|
props: {
|
||||||
props: {
|
product: data
|
||||||
product: data.data!
|
|
||||||
},
|
|
||||||
revalidate: 10 * 60
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
revalidate: 10 * 60
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,304 +1,237 @@
|
||||||
import {
|
import {
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Button,
|
Button, Skeleton, Accordion, AccordionItem, Pagination, Spinner
|
||||||
Skeleton,
|
|
||||||
Accordion,
|
|
||||||
AccordionItem,
|
|
||||||
Pagination,
|
|
||||||
Spinner,
|
|
||||||
} from "@nextui-org/react";
|
} from "@nextui-org/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { InferGetStaticPropsType } from "next";
|
import {InferGetStaticPropsType} from "next";
|
||||||
import FavouriteIcon from "@/../public/favourites_icon.svg";
|
import FavouriteIcon from "@/../public/favourites_icon.svg";
|
||||||
import { toggleFavourite } from "@/store/favourites";
|
import {toggleFavourite} from "@/store/favourites";
|
||||||
import { useSnapshot } from "valtio";
|
import {useSnapshot} from "valtio";
|
||||||
|
|
||||||
import favouritesStore from "@/store/favourites";
|
import favouritesStore from "@/store/favourites";
|
||||||
import useClient from "@/hooks/useClient";
|
import useClient from "@/hooks/useClient";
|
||||||
import Wrapper from "@/components/reusable/wrapper";
|
import Wrapper from "@/components/reusable/wrapper";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
|
||||||
import LocalAPI from "@/service/localAPI";
|
import LocalAPI from "@/service/localAPI";
|
||||||
import { Img } from "react-image";
|
import {Img} from "react-image";
|
||||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
import {Dispatch, SetStateAction, useEffect, useState} from "react";
|
||||||
import { Product } from "@/service/types/local";
|
import {element} from "prop-types";
|
||||||
|
|
||||||
type SelectedFiltersStruct = {
|
type SelectedFiltersStruct = {
|
||||||
[key: string]: string[];
|
[key: string]: string[]
|
||||||
};
|
}
|
||||||
type SelectedFiltersDispatcher = Dispatch<
|
type SelectedFiltersDispatcher = Dispatch<SetStateAction<SelectedFiltersStruct>>
|
||||||
SetStateAction<SelectedFiltersStruct>
|
|
||||||
>;
|
|
||||||
|
|
||||||
const CheckboxUI = (
|
const CheckboxUI = (obj: { id: number, code: string, name: string, values: { id: number, value: string }[] } & {
|
||||||
obj: {
|
dispatcher: SelectedFiltersDispatcher
|
||||||
id: number;
|
|
||||||
code: string;
|
|
||||||
name: string;
|
|
||||||
values: { id: number; value: string }[];
|
|
||||||
} & {
|
|
||||||
dispatcher: SelectedFiltersDispatcher;
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<Accordion>
|
|
||||||
<AccordionItem title={obj.name}>
|
|
||||||
<CheckboxGroup>
|
|
||||||
{obj.values &&
|
|
||||||
obj.values.map((val) => (
|
|
||||||
<Checkbox
|
|
||||||
onChange={() => {
|
|
||||||
obj.dispatcher((prevState) => {
|
|
||||||
if (
|
|
||||||
prevState[obj.code] &&
|
|
||||||
prevState[obj.code].includes(val.value)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
[obj.code]: prevState[obj.code].filter(
|
|
||||||
(v) => v !== val.value,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
[obj.code]: [...(prevState[obj.code] || []), val.value],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
value={String(val.id)}
|
|
||||||
key={"VALUE_" + val.id}
|
|
||||||
>
|
|
||||||
{val.value}
|
|
||||||
</Checkbox>
|
|
||||||
))}
|
|
||||||
</CheckboxGroup>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const FilterGenerator = ({
|
|
||||||
filterPropertiesData,
|
|
||||||
setSelectedFilter,
|
|
||||||
}: Pick<
|
|
||||||
InferGetStaticPropsType<typeof getStaticProps>,
|
|
||||||
"filterPropertiesData"
|
|
||||||
> & {
|
|
||||||
setSelectedFilter: SelectedFiltersDispatcher;
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
|
||||||
<Accordion
|
|
||||||
className={"filters mb-10 mr-4 w-full lg:w-1/4"}
|
|
||||||
defaultExpandedKeys={["1"]}
|
|
||||||
>
|
|
||||||
<AccordionItem
|
|
||||||
title={"Фильтры"}
|
|
||||||
key={"1"}
|
|
||||||
aria-label={"Фильтры"}
|
|
||||||
classNames={{
|
|
||||||
trigger: "bg-primary px-4",
|
|
||||||
base: "border-1 rounded",
|
|
||||||
title: "font-bold",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filterPropertiesData.map((obj) => (
|
|
||||||
<CheckboxUI key={obj.id} {...obj} dispatcher={setSelectedFilter} />
|
|
||||||
))}
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CatalogCard = (product: Product & { isFavourite: boolean }) => {
|
return (
|
||||||
const isClient = useClient();
|
<Accordion>
|
||||||
const cartItems = useQuery({
|
<AccordionItem title={obj.name}>
|
||||||
queryKey: ["cart"],
|
<CheckboxGroup>
|
||||||
queryFn: async () => {
|
{obj.values && obj.values.map(val => (
|
||||||
const service = new LocalAPI();
|
<Checkbox onChange={
|
||||||
return await service.getCartItems();
|
() => {
|
||||||
},
|
obj.dispatcher((prevState) => {
|
||||||
});
|
|
||||||
|
|
||||||
const qs = useQueryClient();
|
if (prevState[obj.code] && prevState[obj.code].includes(val.value)) {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
[obj.code]: prevState[obj.code].filter(v => v !== val.value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
[obj.code]: [...(prevState[obj.code] || []), val.value]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} value={String(val.id)} key={"VALUE_" + val.id}>{val.value}</Checkbox>
|
||||||
|
))}
|
||||||
|
</CheckboxGroup>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
const toggleCart = useMutation({
|
</Accordion>
|
||||||
mutationFn: async ({
|
)
|
||||||
productId,
|
}
|
||||||
quantity,
|
const FilterGenerator = ({
|
||||||
}: {
|
filterPropertiesData,
|
||||||
productId: number;
|
setSelectedFilter
|
||||||
quantity: number;
|
}: Pick<InferGetStaticPropsType<typeof getStaticProps>, "filterPropertiesData"> & {
|
||||||
}) => {
|
setSelectedFilter: SelectedFiltersDispatcher
|
||||||
const service = new LocalAPI();
|
}) => {
|
||||||
if (
|
return (
|
||||||
cartItems.data &&
|
<Accordion className={'filters mb-10 mr-4 lg:w-1/4 w-full'} defaultExpandedKeys={['1']}>
|
||||||
cartItems.data.data &&
|
<AccordionItem title={"Фильтры"} key={"1"} aria-label={"Фильтры"}
|
||||||
cartItems.data.data.find((item ) => item.product.id === productId)
|
classNames={{trigger: "bg-primary px-4", base: "border-1 rounded", title: "font-bold"}}>
|
||||||
) {
|
{filterPropertiesData.map(obj => (
|
||||||
return await service.deleteCartItem(productId);
|
<CheckboxUI key={obj.id} {...obj} dispatcher={setSelectedFilter}/>
|
||||||
}
|
))}
|
||||||
return await service.addCartItem(productId, quantity);
|
</AccordionItem>
|
||||||
},
|
</Accordion>
|
||||||
onSuccess: () => {
|
)
|
||||||
qs.invalidateQueries({ queryKey: ["cart"] });
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
type CatalogItemStruct = {
|
||||||
<div
|
id: number
|
||||||
className={
|
code: string,
|
||||||
"bg-gray-card h-fit min-h-[250px] w-full rounded-[20px] px-7 py-4 transition-shadow hover:cursor-pointer hover:shadow-md"
|
name: string,
|
||||||
}
|
properties: {
|
||||||
key={product.id}
|
[key: string]: string | string[]
|
||||||
>
|
}
|
||||||
<div
|
detailText: string,
|
||||||
className={
|
price: {
|
||||||
"relative grid w-fit grid-cols-1 items-center sm:grid-cols-10"
|
[key: string]: number | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type CardProps = CatalogItemStruct & { isFavourite?: boolean }
|
||||||
|
|
||||||
|
|
||||||
|
const CatalogCard = (product: CardProps) => {
|
||||||
|
const isClient = useClient()
|
||||||
|
const cartItems = useQuery({
|
||||||
|
queryKey: ['cart'], queryFn: async () => {
|
||||||
|
const service = new LocalAPI()
|
||||||
|
return await service.getCartItems()
|
||||||
}
|
}
|
||||||
>
|
})
|
||||||
{isClient && product.properties.main_image && (
|
|
||||||
<Img
|
|
||||||
src={`https://relynolli.ru/upload/${product.properties.main_image[0]}`}
|
const qs = useQueryClient()
|
||||||
alt={product.name}
|
|
||||||
className={"col-auto mx-auto mb-4 sm:col-span-2 sm:row-span-2 max-h-[250px]"}
|
const toggleCart = useMutation({
|
||||||
loader={<Spinner />}
|
mutationFn: async ({productId, quantity}: { productId: number, quantity: number }) => {
|
||||||
/>
|
const service = new LocalAPI()
|
||||||
)}
|
if (cartItems.data!.find(({id}) => id === productId)) {
|
||||||
<div className="col-auto flex h-full flex-col justify-center sm:col-span-6 sm:col-start-4">
|
return await service.deleteCartItem(productId)
|
||||||
<span
|
}
|
||||||
className={"text-subtitle-4 mb-2 block font-normal text-[#52525C]"}
|
return await service.addCartItem(productId, quantity)
|
||||||
>
|
},
|
||||||
Стандарт API: {product.properties.api_standart} Тип:{" "}
|
onSuccess: () => {
|
||||||
{product.properties.oil_type}
|
qs.invalidateQueries({queryKey: ["cart"]})
|
||||||
</span>
|
}
|
||||||
<h3 className={"text-black-3 text-lg font-bold uppercase"}>
|
})
|
||||||
{product.name}
|
|
||||||
</h3>
|
|
||||||
|
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"}>
|
||||||
|
{
|
||||||
|
isClient && product.properties.main_image &&
|
||||||
|
<Img src={`https://relynolli.ru/upload/${product.properties.main_image[0]}`} alt={product.name}
|
||||||
|
className={"col-auto mx-auto mb-4 sm:col-span-2 sm:row-span-2"}
|
||||||
|
loader={<Spinner/>}/>
|
||||||
|
}
|
||||||
|
<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}`.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')} ₽
|
||||||
|
</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>
|
</div>
|
||||||
{isClient ? (
|
)
|
||||||
<FavouriteIcon
|
}
|
||||||
onClick={() => toggleFavourite(product.id)}
|
|
||||||
className={`absolute right-0 top-0 z-20 transition-colors ${product.isFavourite ? "fill-primary" : "fill-[#E0E3E3]"}`}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="col-auto row-auto flex w-full items-center justify-between pt-4 sm:col-span-7 sm:col-start-4 sm:row-start-2">
|
|
||||||
<span className="text-black-3 text-xl font-bold">
|
|
||||||
{`${product.price.BASE}`.replace(/\B(?=(\d{3})+(?!\d))/g, " ")} ₽
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{(cartItems.data) ? (
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
toggleCart.mutate({
|
|
||||||
productId: product.id,
|
|
||||||
quantity: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className={
|
|
||||||
"bg-green-2 text-black-3 relative z-20 text-lg font-bold uppercase italic"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(cartItems.data && cartItems.data.data && cartItems.data.data
|
|
||||||
.map(({ productId }) => productId)
|
|
||||||
.includes(product.id))
|
|
||||||
? "В корзине"
|
|
||||||
: "В корзину"}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href={"/catalog/" + product.code}
|
|
||||||
className={"absolute left-0 top-0 z-10 h-full w-full"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Catalog = (props: InferGetStaticPropsType<typeof getStaticProps>) => {
|
const Catalog = (props: InferGetStaticPropsType<typeof getStaticProps>) => {
|
||||||
const { favourites } = useSnapshot(favouritesStore);
|
|
||||||
const [selectedFilters, setSelectedFilters] = useState<SelectedFiltersStruct>(
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [totalPageCount, setTotalPageCount] = useState(1);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const {favourites} = useSnapshot(favouritesStore)
|
||||||
setPage(1);
|
const [selectedFilters, setSelectedFilters] = useState<SelectedFiltersStruct>({})
|
||||||
}, [selectedFilters]);
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageCount, setPageCount] = useState(1)
|
||||||
|
const [showPagination, setShowPagination] = useState(true);
|
||||||
|
|
||||||
const catalogQuery = useQuery({
|
|
||||||
queryKey: ["catalog", { selectedFilters, page }],
|
|
||||||
queryFn: async () => {
|
|
||||||
const service = new LocalAPI();
|
|
||||||
const data = await service.getCatalogItems(selectedFilters, page);
|
|
||||||
setTotalPageCount(Math.ceil(data.meta.count! / 10));
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<Wrapper
|
const service = new LocalAPI()
|
||||||
title={"Каталог"}
|
service.getCatalogItemsCount().then(data => setPageCount(Math.ceil(data / 10)))
|
||||||
breadcrumbs={[{ name: "Каталог", link: "/catalog" }]}
|
|
||||||
>
|
|
||||||
<div className="mb-4 flex justify-end">
|
|
||||||
<Pagination
|
|
||||||
page={page}
|
|
||||||
total={totalPageCount}
|
|
||||||
initialPage={1}
|
|
||||||
onChange={(page) => {
|
|
||||||
setPage(page);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-between lg:flex-row">
|
|
||||||
<FilterGenerator
|
|
||||||
filterPropertiesData={props.filterPropertiesData}
|
|
||||||
setSelectedFilter={setSelectedFilters}
|
|
||||||
/>
|
|
||||||
<div className="products grid h-fit w-full grid-cols-1 gap-7 lg:w-3/4 2xl:grid-cols-2">
|
|
||||||
{catalogQuery.isFetching &&
|
|
||||||
Array(10)
|
|
||||||
.fill(null)
|
|
||||||
.map((_, index) => (
|
|
||||||
<Skeleton key={index} className={"h-[250px] rounded-[20px]"} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{catalogQuery.data &&
|
|
||||||
catalogQuery.data.data!.map((product) => (
|
})
|
||||||
<>
|
|
||||||
<CatalogCard
|
const catalogQuery = useQuery<CatalogItemStruct[]>({
|
||||||
key={product.id}
|
queryKey: ["catalog", {selectedFilters, page}], queryFn: async () => {
|
||||||
{...product}
|
const service = new LocalAPI()
|
||||||
isFavourite={favourites.includes(product.id)}
|
return await service.getCatalogItems(selectedFilters, page)
|
||||||
/>
|
}
|
||||||
</>
|
})
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
return (<Wrapper title={"Каталог"} breadcrumbs={[{name: "Каталог", link: "/catalog"}]}>
|
||||||
</Wrapper>
|
<div className="flex justify-end mb-4">
|
||||||
);
|
{ !Object.values(selectedFilters).filter(elem => elem.length).length &&
|
||||||
};
|
<Pagination total={pageCount} initialPage={1} onChange={(page) => {
|
||||||
|
setPage(page)
|
||||||
|
}}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-between lg:flex-row">
|
||||||
|
<FilterGenerator filterPropertiesData={props.filterPropertiesData}
|
||||||
|
setSelectedFilter={setSelectedFilters}/>
|
||||||
|
<div className="products grid grid-cols-1 2xl:grid-cols-2 gap-7 lg:w-3/4 h-fit w-full">
|
||||||
|
|
||||||
|
{catalogQuery.isFetching &&
|
||||||
|
Array(10).fill(null).map((_, index) => <Skeleton key={index}
|
||||||
|
className={"h-[250px] rounded-[20px]"}/>)
|
||||||
|
}
|
||||||
|
|
||||||
|
{catalogQuery.data &&
|
||||||
|
catalogQuery.data.map(product =>
|
||||||
|
<>
|
||||||
|
<CatalogCard key={product.id} {...product}
|
||||||
|
isFavourite={favourites.includes(product.id)}/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default Catalog;
|
export default Catalog;
|
||||||
|
|
||||||
export const getStaticProps = async () => {
|
export const getStaticProps = async () => {
|
||||||
const service = new LocalAPI();
|
const service = new LocalAPI()
|
||||||
const filterData = (await service.getFilters()) as {
|
const filterData = await service.getFilters() as {
|
||||||
id: number;
|
id: number,
|
||||||
name: string;
|
name: string,
|
||||||
code: string;
|
code: string,
|
||||||
values: { id: number; value: string }[];
|
values: { id: number, value: string }[]
|
||||||
}[];
|
}[]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
filterPropertiesData: filterData,
|
filterPropertiesData: filterData,
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
};
|
}
|
|
@ -1,583 +0,0 @@
|
||||||
import Wrapper from "@/components/reusable/wrapper";
|
|
||||||
import Stepper from "@/components/reusable/stepper";
|
|
||||||
import CallbackForm from "@/components/reusable/contacts/callbackForm";
|
|
||||||
|
|
||||||
const Form1 = () => (
|
|
||||||
<>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
const Form2 = () => (<></>)
|
|
||||||
|
|
||||||
const Form3 = () => (<></>)
|
|
||||||
|
|
||||||
const Form4= () => (<></>)
|
|
||||||
|
|
||||||
const Form5 = () => (<></>)
|
|
||||||
|
|
||||||
const Form6 = () => (<></>)
|
|
||||||
|
|
||||||
const Form7 = () => (<></>)
|
|
||||||
|
|
||||||
const Form8 = () => (<></>)
|
|
||||||
|
|
||||||
const Form9 = () => (<></>)
|
|
||||||
|
|
||||||
const Form10 = () => (<></>)
|
|
||||||
|
|
||||||
const Form11 = () => (<></>)
|
|
||||||
|
|
||||||
const Form12 = () => (<></>)
|
|
||||||
const Form13 = () => (<></>)
|
|
||||||
|
|
||||||
const Form14 = () => (<></>)
|
|
||||||
|
|
||||||
const Contact = () => {
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
curStep: "1",
|
|
||||||
title: "Покупатель",
|
|
||||||
value: "customer",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep: "2",
|
|
||||||
title: "Юридическое лицо",
|
|
||||||
value: "business",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Помощь подбора масла",
|
|
||||||
value: "oil-select-help",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form1
|
|
||||||
}
|
|
||||||
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Уточнение характеристик товара",
|
|
||||||
value: "product-characteristics",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Заявка на сотрудничество",
|
|
||||||
value: "cooperation",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Обращение к техническому специалисту",
|
|
||||||
value: "technical-specialist",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Запрос сертификата",
|
|
||||||
value: "certificate-request",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Отзыв о качестве продукции",
|
|
||||||
value: "feedback-on-product-quality",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Жалоба",
|
|
||||||
value: "complaint",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Иные вопросы",
|
|
||||||
value: "other-questions",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Оформить подписку",
|
|
||||||
value: "subscribe",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form5
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep: "2",
|
|
||||||
title: "Физическое лицо",
|
|
||||||
value: "individual",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Помощь подбора масла",
|
|
||||||
value: "oil-select-help",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form6
|
|
||||||
}
|
|
||||||
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Уточнение характеристик товара",
|
|
||||||
value: "product-characteristics",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form9
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Заявка на сотрудничество",
|
|
||||||
value: "cooperation",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form7
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Обращение к техническому специалисту",
|
|
||||||
value: "technical-specialist",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form6
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Запрос сертификата",
|
|
||||||
value: "certificate-request",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form7
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Отзыв о качестве продукции",
|
|
||||||
value: "feedback-on-product-quality",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form8
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Жалоба",
|
|
||||||
value: "complaint",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form8
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Иные вопросы",
|
|
||||||
value: "other-questions",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form8
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Оформить подписку",
|
|
||||||
value: "subscribe",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form10
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep: "1",
|
|
||||||
title: "Поставщик",
|
|
||||||
value : "supplier",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep: "2",
|
|
||||||
title: "Юридическое лицо",
|
|
||||||
value: "business",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Помощь подбора масла",
|
|
||||||
value: "oil-select-help",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form1
|
|
||||||
}
|
|
||||||
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Уточнение характеристик товара",
|
|
||||||
value: "product-characteristics",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Заявка на сотрудничество",
|
|
||||||
value: "cooperation",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Обращение к техническому специалисту",
|
|
||||||
value: "technical-specialist",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Запрос сертификата",
|
|
||||||
value: "certificate-request",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Отзыв о качестве продукции",
|
|
||||||
value: "feedback-on-product-quality",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Жалоба",
|
|
||||||
value: "complaint",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Иные вопросы",
|
|
||||||
value: "other-questions",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Оформить подписку",
|
|
||||||
value: "subscribe",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form5
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep: "2",
|
|
||||||
title: "Физическое лицо",
|
|
||||||
value: "individual",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Помощь подбора масла",
|
|
||||||
value: "oil-select-help",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form6
|
|
||||||
}
|
|
||||||
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Уточнение характеристик товара",
|
|
||||||
value: "product-characteristics",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form9
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Заявка на сотрудничество",
|
|
||||||
value: "cooperation",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form7
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Обращение к техническому специалисту",
|
|
||||||
value: "technical-specialist",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form6
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Запрос сертификата",
|
|
||||||
value: "certificate-request",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form7
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Отзыв о качестве продукции",
|
|
||||||
value: "feedback-on-product-quality",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form8
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Жалоба",
|
|
||||||
value: "complaint",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form8
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Иные вопросы",
|
|
||||||
value: "other-questions",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form8
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep:"3",
|
|
||||||
title: "Оформить подписку",
|
|
||||||
value: "subscribe",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form10
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep: "1",
|
|
||||||
title: "Государственное предприятие",
|
|
||||||
value : "state-enterprise",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep: "2",
|
|
||||||
title: "Закупщик",
|
|
||||||
value: "buyer",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep:"4",
|
|
||||||
component: Form11,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep: "2",
|
|
||||||
title: "Учебное или научное заведение",
|
|
||||||
value: "university",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep: "4",
|
|
||||||
component: Form12
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep: "1",
|
|
||||||
title: "Работник компании",
|
|
||||||
value : "worker",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep: "2",
|
|
||||||
title: "Соискатель",
|
|
||||||
value: "founder",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep: "4",
|
|
||||||
component: Form13
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
curStep: "2",
|
|
||||||
title: "Штатный сотрудник",
|
|
||||||
value: "local-worker",
|
|
||||||
step: [
|
|
||||||
{
|
|
||||||
curStep: "4",
|
|
||||||
component:Form14
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper title={"Контакты"} breadcrumbs={[{name: "Контакты", link: "/contacts"}]}>
|
|
||||||
<iframe className={"rounded-[20px] mb-24"} src="https://yandex.ru/map-widget/v1/?um=constructor%3Aa9c71bba18ffe78a2028d1851bd205ecebc04063c76af8dedab6bceff5c73b24&source=constructor" width="100%" height="500"></iframe>
|
|
||||||
|
|
||||||
{/*<div className={"block mb-24"}>*/}
|
|
||||||
{/* <Stepper steps={[*/}
|
|
||||||
{/* {title: "1", isActive: true},*/}
|
|
||||||
{/* {title: "2", isActive: false},*/}
|
|
||||||
{/* {title: "3", isActive: false},*/}
|
|
||||||
{/* {title: "4", isActive: false},*/}
|
|
||||||
{/* ]}></Stepper>*/}
|
|
||||||
|
|
||||||
{/* <CallbackForm steps={steps.map(step => ({title: step.title, value: step.value}))}>*/}
|
|
||||||
|
|
||||||
{/* </CallbackForm>*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
|
|
||||||
|
|
||||||
<div className={"grid gap-5 grid-cols-1 lg:grid-cols-2 text-center mb-24"}>
|
|
||||||
<div className={"bg-[#E2E2E5] p-10 rounded-[20px]"}>
|
|
||||||
<span className={"block font-bold text-xl mb-4 max-w-[520px] mx-auto"}>Московская область, г. Домодедово, Каширское ш, 4, к.1, оф.330</span>
|
|
||||||
<span className={"block text-[#52525C] opacity-60"}>Адрес</span>
|
|
||||||
</div>
|
|
||||||
<div className={"bg-[#E2E2E5] p-10 rounded-[20px]"}>
|
|
||||||
<span className={"block font-bold text-xl mb-4 max-w-[520px] mx-auto"}>142000, Московская обл,
|
|
||||||
г. Домодедово, а/а 80</span>
|
|
||||||
<span className={"block text-[#52525C] opacity-60"}>Адрес для писем</span>
|
|
||||||
</div>
|
|
||||||
<div className={"bg-[#E2E2E5] p-10 rounded-[20px]"}>
|
|
||||||
<span className={"block font-bold text-xl mb-4 max-w-[520px] mx-auto"}>+7 495 191-97-20</span>
|
|
||||||
<span className={"block text-[#52525C] opacity-60"}>Телефон</span>
|
|
||||||
</div>
|
|
||||||
<div className={"bg-[#E2E2E5] p-10 rounded-[20px]"}>
|
|
||||||
<span className={"block font-bold text-xl mb-4 max-w-[520px] mx-auto"}>9.00-19.00</span>
|
|
||||||
<span className={"block text-[#52525C] opacity-60"}>Часы работы</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</Wrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default Contact;
|
|
|
@ -1,205 +1,86 @@
|
||||||
import {Button, Divider, dropdownSection, Spinner, Tooltip} from "@nextui-org/react";
|
import {Button, dropdownSection} from "@nextui-org/react";
|
||||||
import ChevronBannerIcon from "@/../public/banner_arr_btn.svg.svg"
|
import ChevronBannerIcon from "@/../public/banner_arr_btn.svg.svg"
|
||||||
import {Card, CardHeader, Image} from "@nextui-org/react";
|
import {Card, CardHeader, Image} from "@nextui-org/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {Swiper, SwiperSlide} from "swiper/react";
|
import NextImage from "next/image";
|
||||||
import {Pagination, Navigation, Autoplay} from "swiper/modules";
|
|
||||||
|
|
||||||
import "swiper/css"
|
|
||||||
import 'swiper/css/navigation';
|
|
||||||
import 'swiper/css/pagination';
|
|
||||||
import "swiper/css/autoplay";
|
|
||||||
import * as http2 from "http2";
|
|
||||||
import {Img} from "react-image";
|
|
||||||
import {News, Wrapper} from "@/service/types/local";
|
|
||||||
import useClient from "@/hooks/useClient";
|
|
||||||
import {useQuery} from "@tanstack/react-query";
|
|
||||||
import LocalAPI from "@/service/localAPI";
|
|
||||||
|
|
||||||
const Hero = () => {
|
const Hero = () => {
|
||||||
return (
|
return (
|
||||||
<Swiper
|
<section className={"w-full bg-cars bg-no-repeat h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right"}>
|
||||||
slidesPerView={1}
|
<div className={"wrapper h-full flex flex-col justify-center"}>
|
||||||
spaceBetween={0}
|
<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]">
|
||||||
className={"[&_.swiper-button-next]:after:text-primary [&_.swiper-button-next]:after:content-['next'] [&_.swiper-button-prev]:after:text-primary [&_.swiper-button-prev]:after:content-['prev']"}
|
<h2 className={"text-2xl leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-[52px]"}>Оформите заказ
|
||||||
modules={[Navigation, Pagination, Autoplay]}
|
на сумму от 5000₽</h2>
|
||||||
speed={500}
|
<span className={"text-sm xl:text-subtitle-1 leading-inherit text-subtitle-1 mb-4 xl:mb-24"}>И получите бесплатную доставку</span>
|
||||||
|
|
||||||
autoplay={
|
<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>
|
||||||
delay: 5000,
|
<ChevronBannerIcon className={"stroke-[3px] stroke-black"} />
|
||||||
disableOnInteraction: false
|
</Link>
|
||||||
}
|
|
||||||
}
|
|
||||||
navigation={
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
pagination={{clickable: true}}
|
|
||||||
|
|
||||||
>
|
<span className={"text-white opacity-60 text-sm mt-2"}>*Срок действия акции ограничен</span>
|
||||||
<SwiperSlide>
|
</div>
|
||||||
<section
|
|
||||||
className={"w-full h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}>
|
|
||||||
<img src={"/cars.PNG"} className={"object-cover h-full w-full z-10 absolute"} alt={"cars"}/>
|
|
||||||
<div className={"wrapper h-full flex flex-col justify-center relative z-20"}>
|
|
||||||
<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-xl uppercase leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-8"}>Моторные
|
|
||||||
масла для <span className={"text-primary"}>дизельных</span> двигателей</h2>
|
|
||||||
<span className={"text-[1.14rem] xl:text-subtitle-1 leading-inherit mb-4 xl:mb-6"}>Грузового транспорта и спецтехники</span>
|
|
||||||
|
|
||||||
<Link href={"/catalog"}
|
</div>
|
||||||
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'}>
|
</section>
|
||||||
<span
|
|
||||||
className={"text-sm text-[1.125rem] font-extrabold italic uppercase"}>Купить</span>
|
|
||||||
<ChevronBannerIcon className={"stroke-[3px] stroke-black"}/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide>
|
|
||||||
<section
|
|
||||||
className={"w-full bg-no-repeat h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}>
|
|
||||||
<img src={"/hands.PNG"} className={"object-cover h-full w-full z-10 absolute"} alt={"hands"}/>
|
|
||||||
<div className={"wrapper h-full flex flex-col justify-center relative z-20"}>
|
|
||||||
<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-[1.14rem] uppercase leading-[100%] xl:text-title-4 2xl:text-title-3 mb-4 xl:mb-8"}>
|
|
||||||
<span className={"2xl:text-title-3 text-primary"}>Начать бизнес</span> по продажам
|
|
||||||
качественного моторного масла </h2>
|
|
||||||
<span
|
|
||||||
className={"text-xl uppercase xl:text-title-2 leading-inherit text-subtitle-1 mb-4 xl:mb-6 text-primary"}>-ЛЕГКО</span>
|
|
||||||
<Link href={"https://forms.yandex.ru/u/65e4c1e9eb6146024f8e3234/"}
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide key={Math.random()}>
|
|
||||||
<section
|
|
||||||
className={"w-full bg-cars bg-no-repeat h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}>
|
|
||||||
<img src={"/sto.PNG"} className={"object-cover h-full w-full z-10 absolute"} alt={"auto services"}/>
|
|
||||||
<div className={"wrapper h-full flex flex-col justify-center z-20 relative"}>
|
|
||||||
<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-xl leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-8"}>Приглашаем
|
|
||||||
к сотрудничеству СТО и автосервисы</h2>
|
|
||||||
<span className={"text-sm italic leading-[100%] xl:text-subtitle-1 mb-4 xl:mb-6"}>Поставляем масло <span
|
|
||||||
className={"text-primary"}>RELYNOLLI ®</span> в партнерские СТО на льготных условиях</span>
|
|
||||||
|
|
||||||
<Link href={"https://forms.yandex.ru/u/65e4c1e9eb6146024f8e3234/"}
|
|
||||||
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>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</SwiperSlide>
|
|
||||||
</Swiper>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const MainInfo = () => {
|
const MainInfo = () => {
|
||||||
return (
|
return (
|
||||||
<section className={"bg-white text-black rounded-[8px]"}>
|
<section className={"bg-white text-black rounded-[8px]"}>
|
||||||
<div className="wrapper py-8 mb-8">
|
<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"}>Моторные
|
<h1 className={"text-4xl xl:text-title-1 leading-normal font-bold text-center italic uppercase text-black mb-8"}>Моторные масла и смазочные материалы <br/>
|
||||||
масла и смазочные материалы <br/>
|
|
||||||
<span className={"text-primary"}>Relynolli ®</span></h1>
|
<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>
|
<span className={"text-base lg:text-2xl xl:text-4xl text-center block w-1/2 mx-auto"}>Технологичные решения для надежной работы двигателя</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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%]">
|
||||||
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={"lg:hover:scale-105 relative group lg:grayscale hover:grayscale-0 transition-all"}>
|
<Card className={"rounded-[30px] hover:scale-105 transition-size"}>
|
||||||
<Link href={"/catalog"} className={"absolute top-0 left-0 z-20 w-full h-full"}/>
|
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil2.png"}></Image>
|
||||||
<Image removeWrapper className={"w-full h-full object-cover brightness-75 relative z-10"}
|
|
||||||
src={"/oil2.png"}></Image>
|
|
||||||
<CardHeader
|
<CardHeader
|
||||||
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
|
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
|
||||||
<h3 className={"text-3xl lg:text-title-3 leading-[100%] text-white lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Масла
|
<h3 className={"text-title-3 leading-[35px] text-white"}>Масла для легковых автомобилей</h3>
|
||||||
для легковых автомобилей</h3>
|
|
||||||
<span
|
<span className={"text-gray-2"}>Серия Standart и Premium для легковых автомобилей для наилучшей энергоэффективности двигателя вашего автомобиля</span>
|
||||||
className={"text-gray-2 lg:opacity-0 lg:translate-y-1/2 lg:group-hover:opacity-100 lg:group-hover:translate-y-0 transition-all delay-100"}>Серия Standart и Premium для легковых автомобилей для наилучшей энергоэффективности двигателя вашего автомобиля</span>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Tooltip content={"Выпуск Апрель 2024"} className={"text-black"} size={"lg"}>
|
<Card className={"rounded-[30px] hover:scale-105 transition-size"}>
|
||||||
<Card className={"rounded-[30px] group transition-size"}>
|
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil1.png"}></Image>
|
||||||
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50 grayscale"}
|
<CardHeader
|
||||||
src={"/oil1.png"}></Image>
|
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
|
||||||
<CardHeader
|
<h3 className={"text-title-3 leading-[35px] text-white"}>Специальная серия масел XMR</h3>
|
||||||
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
|
|
||||||
<h3 className={"text-3xl lg:text-title-3 leading-[100%] text-white lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Специальная
|
|
||||||
серия масел XMR</h3>
|
|
||||||
|
|
||||||
<span
|
<span className={"text-gray-2"}>Серия моторных масел XMR - премиальная линейка для двигателей со спортивным характером</span>
|
||||||
className={"text-gray-2 lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all delay-100"}>Серия моторных масел XMR - премиальная линейка для двигателей со спортивным характером</span>
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<span
|
<Card className={"rounded-[30px] hover:scale-105 transition-size"}>
|
||||||
className={"block bg-primary text-black absolute top-[-10%] left-0 w-full font-bold lg:hidden px-4 py-2"}>
|
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil3.png"}></Image>
|
||||||
Выпуск Апрель 2024
|
<CardHeader
|
||||||
</span>
|
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
|
||||||
</CardHeader>
|
<h3 className={"text-title-3 leading-[35px] text-white"}>Масла для коммерческого транспорта и спецтехники</h3>
|
||||||
</Card>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
|
<span className={"text-gray-2"}>Синтетические и полусинтетические масла для двигателей, которые эксплуатируются под высокими нагрузками. Подходят для бензиновых и дизельных двигателей</span>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Tooltip content={"Выпуск Июнь 2024"} className={"text-black"} size={"lg"}>
|
<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>
|
||||||
|
|
||||||
<Card className={"rounded-[30px] transition-size group"}>
|
<span className={"text-gray-2"}>Масла для четырех и двухтактных двигателей, которые помогут раскрыть неудержимый характер вашей мототехники</span>
|
||||||
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50 grayscale"}
|
</CardHeader>
|
||||||
src={"/oil3.png"}></Image>
|
</Card>
|
||||||
<CardHeader
|
|
||||||
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
|
|
||||||
<h3 className={"text-3xl lg:text-title-3 leading-[100%] text-white lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Масла
|
|
||||||
для коммерческого транспорта и спецтехники</h3>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className={"text-gray-2 lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all delay-100"}>Синтетические и полусинтетические масла для двигателей, которые эксплуатируются под высокими нагрузками. Подходят для бензиновых и дизельных двигателей</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className={"block bg-primary text-black absolute top-[-10%] left-0 w-full font-bold lg:hidden px-4 py-2"}>
|
|
||||||
Выпуск Апрель 2024
|
|
||||||
</span>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
|
|
||||||
<Tooltip content={"Выпуск Апрель 2024"} className={"text-black"} size={"lg"}>
|
|
||||||
<Card className={"rounded-[30px] transition-size group"}>
|
|
||||||
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50 grayscale"}
|
|
||||||
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-3xl lg:text-title-3 leading-[100%] text-white lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Масла
|
|
||||||
для мототехники</h3>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className={"text-gray-2 lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all delay-100"}>Масла для четырех и двухтактных двигателей, которые помогут раскрыть неудержимый характер вашей мототехники</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className={"block bg-primary text-black absolute top-[-10%] left-0 w-full font-bold lg:hidden px-4 py-2"}>
|
|
||||||
Выпуск Апрель 2024
|
|
||||||
</span>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -210,12 +91,10 @@ const Achievements = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={"bg-cover backdrop-brightness-50 relative"}>
|
<section className={"bg-cover backdrop-brightness-50 relative"}>
|
||||||
<Image removeWrapper alt={"oiltypeImage"}
|
<Image removeWrapper alt={"oiltypeImage"} className={"absolute top-0 left-0 w-full h-full brightness-50 object-fill -z-10"} src={"/oiltypeImage.png"}></Image>
|
||||||
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="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]">
|
<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 transition-size"}>
|
<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"}
|
<Image removeWrapper className={"z-0 w-full h-full object-cover"}
|
||||||
src={"/achievements1.png"}></Image>
|
src={"/achievements1.png"}></Image>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
|
@ -235,129 +114,35 @@ const Achievements = () => {
|
||||||
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className={"z-10 relative rounded-[30px] font-bold [&_span]:font-normal transition-size"}>
|
<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"}
|
<Image removeWrapper className={"z-0 w-full h-full object-cover"}
|
||||||
src={"/achievements2.png"}></Image>
|
src={"/achievements2.png"}></Image>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-1/2 justify-between">
|
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-1/2 justify-between">
|
||||||
|
|
||||||
<div className="block">
|
<div className="block">
|
||||||
<h3 className={"text-2xl md:text-3xl leading-[35px] text-white mb-6 "}>Моторные масла
|
<h3 className={"text-2xl md:text-3xl leading-[35px] text-white mb-6 "}>Моторные масла Relynolli ®</h3>
|
||||||
Relynolli ®</h3>
|
|
||||||
<span className={"text-base md:text-xl text-white opacity-50"}>Обладают высокой смазывающей способностью и обеспечивают надёжную защиту двигателя от износа</span>
|
<span className={"text-base md:text-xl text-white opacity-50"}>Обладают высокой смазывающей способностью и обеспечивают надёжную защиту двигателя от износа</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button className={"bg-green-2 font-bold uppercase italic w-[200px] h-fit text-wrap"}
|
<Button className={"bg-green-2 font-bold uppercase italic"} endContent={<ChevronBannerIcon
|
||||||
endContent={<ChevronBannerIcon
|
className={"stroke-[3px] stroke-black"}/>}>Расшифровка масел группы - N</Button>
|
||||||
className={"stroke-[3px] stroke-black"}/>}>
|
|
||||||
<span
|
|
||||||
className={"block w-2/3 text-left !font-bold italic"}>Расшифровка масел группы - N</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Link className={"mx-auto"} href={"/catalog"}><Button
|
<Button className={"bg-green-2 font-bold uppercase italic mx-auto"} endContent={<ChevronBannerIcon className={"stroke-[3px] stroke-black"}/>}>Перейти в продукцию</Button>
|
||||||
className={"bg-green-2 font-bold uppercase italic"}
|
|
||||||
endContent={<ChevronBannerIcon className={"stroke-[3px] stroke-black"}/>}>Перейти в
|
|
||||||
продукцию</Button></Link>
|
|
||||||
</div>
|
</div>
|
||||||
</section>)
|
</section>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const NewsCard = (news: News) => {
|
|
||||||
return (
|
|
||||||
<Link href={"/news/" + news.code}>
|
|
||||||
|
|
||||||
|
|
||||||
<article className={"h-[300px] px-16 py-4"}>
|
|
||||||
<div className="block h-[200px] overflow-hidden mb-6">
|
|
||||||
<Img className={"object-fill"} src={"https://relynolli.ru/upload/" + news.picture} loader={<Spinner/>}/>
|
|
||||||
</div>
|
|
||||||
<div className="text">
|
|
||||||
<h3 className={"font-bold text-xl"}>{news.name}</h3>
|
|
||||||
<span className={"opacity-50"}>{new Date(news.date).toLocaleDateString()}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</article>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const NewsSlider = () => {
|
|
||||||
const newsData = useQuery<Wrapper<News[]>>({
|
|
||||||
queryKey: ["news"], queryFn: async () => {
|
|
||||||
const service = new LocalAPI()
|
|
||||||
return await service.fetchNews()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<section className={""}>
|
|
||||||
<div className="wrapper">
|
|
||||||
<div className="flex flex-wrap items-center mb-5">
|
|
||||||
<h2 className={"text-2xl md:text-3xl leading-[35px] text-white m-0 mr-4 "}>Новости</h2>
|
|
||||||
<Link
|
|
||||||
className={"block border-primary border-1 text-primary rounded px-4 py-2 italic text-2xl hover:bg-primary hover:text-black transition-colors font-bold"}
|
|
||||||
href={"/news"}>Больше новостей</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{newsData.data &&
|
|
||||||
<Swiper
|
|
||||||
breakpoints={
|
|
||||||
{
|
|
||||||
320: {
|
|
||||||
slidesPerView: 1,
|
|
||||||
spaceBetween: 10
|
|
||||||
},
|
|
||||||
1024: {
|
|
||||||
slidesPerView: 3,
|
|
||||||
spaceBetween: 30
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
// slidesPerView={3}
|
|
||||||
spaceBetween={20}
|
|
||||||
className={"[&_.swiper-button-next]:after:text-primary [&_.swiper-button-next]:after:content-['next'] [&_.swiper-button-prev]:after:text-primary [&_.swiper-button-prev]:after:content-['prev'] min-h-[400px] "}
|
|
||||||
modules={[Navigation, Pagination, Autoplay]}
|
|
||||||
speed={500}
|
|
||||||
|
|
||||||
autoplay={
|
|
||||||
{
|
|
||||||
delay: 5000,
|
|
||||||
disableOnInteraction: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
navigation={
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
pagination={{clickable: true}}
|
|
||||||
|
|
||||||
>
|
|
||||||
{newsData.data.data && newsData.data.data.map((news) => (
|
|
||||||
<SwiperSlide key={news.id}>
|
|
||||||
<NewsCard {...news} />
|
|
||||||
</SwiperSlide>
|
|
||||||
))}
|
|
||||||
|
|
||||||
</Swiper>}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ArticlesSlider = () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero/>
|
<Hero/>
|
||||||
<MainInfo/>
|
<MainInfo/>
|
||||||
<Achievements/>
|
<Achievements/>
|
||||||
<NewsSlider/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
import LocalAPI from "@/service/localAPI";
|
|
||||||
import {InferGetStaticPropsType} from "next";
|
|
||||||
import {Img} from "react-image";
|
|
||||||
import Wrapper from "@/components/reusable/wrapper";
|
|
||||||
|
|
||||||
const News = (props: InferGetStaticPropsType<typeof getStaticProps>) => {
|
|
||||||
return (
|
|
||||||
<Wrapper title={props.news.name} breadcrumbs={[{name: "Новости", link: "/news"}, {name: props.news.name, link: "/news/" + props.news.code}]} >
|
|
||||||
{
|
|
||||||
props.news.picture && <Img className={"max-h-[250px] lg:max-h-[500px] mb-6 mx-auto rounded-[20px]"} src={"https://relynolli.ru/upload/" + props.news.picture} alt={props.news.name} />
|
|
||||||
}
|
|
||||||
<div className="content text-base lg:text-2xl [&>*]:mb-4 [&_img]:max-h-[250px] [&_img]:lg:max-h-[500px] [&_a]:text-primary break-words" dangerouslySetInnerHTML={{__html: props.news.content}}></div>
|
|
||||||
</Wrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default News
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const service = new LocalAPI()
|
|
||||||
const news = await service.fetchNews()
|
|
||||||
return {
|
|
||||||
paths: news.data!.map(item => ({
|
|
||||||
params: {
|
|
||||||
slug: item.code
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
fallback: "blocking",
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStaticProps({params: {slug}}: { params: { slug: string } }) {
|
|
||||||
|
|
||||||
const service = new LocalAPI()
|
|
||||||
try {
|
|
||||||
const news = await service.retrieveNews(slug)
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
news: news.data!
|
|
||||||
},
|
|
||||||
revalidate: 10 * 60
|
|
||||||
}}
|
|
||||||
catch (e) {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
revalidate: 10 * 60
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
import {Img} from "react-image";
|
|
||||||
import {useQuery} from "@tanstack/react-query";
|
|
||||||
import LocalAPI from "@/service/localAPI";
|
|
||||||
import {News} from "@/service/types/local";
|
|
||||||
import {WrapperDark} from "@/components/reusable/wrapper";
|
|
||||||
import {Skeleton} from "@nextui-org/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const NewsCard = (props: News) => {
|
|
||||||
return (
|
|
||||||
<Link href={"/news/" + props.code}>
|
|
||||||
<div className="w-full max-w-[476px] relative group">
|
|
||||||
<div className="h-64 w-full overflow-hidden relative z-20">
|
|
||||||
<Img className="h-full w-full rounded-[20px] mb-5 object-cover"
|
|
||||||
src={"https://relynolli.ru/upload/" + props.picture}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-primary w-full opacity-0 group-hover:opacity-100 absolute top-1/2 left-0 z-10 bottom-0 -translate-y-10 group-hover:translate-y-0 transition-all rounded-b-[20px]"></div>
|
|
||||||
|
|
||||||
<div className="z-20 relative p-2">
|
|
||||||
<div className="w-full text-white group-hover:text-black-2 text-xl font-semibold transition-all">{props.name}</div>
|
|
||||||
<div className="text-neutral-400 text-base font-semibold group-hover:text-black-2 transition-all">{new Date(props.date).toLocaleDateString()}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const News = () => {
|
|
||||||
|
|
||||||
const queryNews = useQuery({
|
|
||||||
queryKey: ["news"], queryFn: async () => {
|
|
||||||
const service = new LocalAPI()
|
|
||||||
return await service.fetchNews()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WrapperDark title={"Новости"} breadcrumbs={[{name: "Новости", link: "/news"}]}>
|
|
||||||
|
|
||||||
<div className="news-container grid grid-cols-1 lg:grid-cols-3 gap-5">
|
|
||||||
{
|
|
||||||
queryNews.data && queryNews.data.data!.map(news => <NewsCard key={news.id} {...news}/>)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
queryNews.isFetching && Array(10).fill(0).map((_, index) => <Skeleton key={index} className={"rounded-[20px] !bg-gray-3"}>
|
|
||||||
<div className="w-full h-64 "></div>
|
|
||||||
</Skeleton> )
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</WrapperDark>)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default News
|
|
|
@ -1,50 +1,138 @@
|
||||||
import Wrapper from "@/components/reusable/wrapper";
|
import Wrapper from "@/components/reusable/wrapper";
|
||||||
import {Radio, RadioGroup} from "@nextui-org/radio";
|
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 {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 {useQuery, useQueryClient} from "@tanstack/react-query";
|
||||||
import {useState} from "react";
|
import {HTMLProps, useEffect, useRef, useState} from "react";
|
||||||
import {SubmitHandler, useForm} from "react-hook-form";
|
import {useForm, Controller, SubmitHandler} from "react-hook-form";
|
||||||
|
import {cmp} from "semver";
|
||||||
import OrderInfo from "@/components/pages/cart/orderInfo";
|
import OrderInfo from "@/components/pages/cart/orderInfo";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {Modal, ModalBody, ModalContent} from "@nextui-org/modal";
|
import {Modal, ModalBody, ModalContent, ModalHeader} from "@nextui-org/modal";
|
||||||
|
import {For} from "@babel/types";
|
||||||
|
import {getUserId} from "@/store/cart";
|
||||||
import {nanoid} from "nanoid";
|
import {nanoid} from "nanoid";
|
||||||
import localAPI from "@/service/localAPI";
|
|
||||||
import {useDebounce} from "@uidotdev/usehooks";
|
|
||||||
import CdekMap from "../../components/pages/order/cdekMap";
|
|
||||||
import {FormDataValuesType} from "@/components/pages/order/types";
|
|
||||||
import FullNameInput from "@/components/pages/order/fullNameInput";
|
|
||||||
import EmailInput from "@/components/pages/order/emailInput";
|
|
||||||
import PhoneInput from "@/components/pages/order/phoneInput";
|
|
||||||
import DeliveryInput from "@/components/pages/order/deliveryTypeInput";
|
|
||||||
import AddressInput from "@/components/pages/order/addressInput";
|
|
||||||
|
|
||||||
const getTakeAwayInfo = async () => {
|
const getTakeAwayInfo = async () => {
|
||||||
return {
|
return (await axios.get("/api/v1/delivery/3")).data
|
||||||
"id": 3,
|
}
|
||||||
"code": "3",
|
|
||||||
"name": "Самовывоз, склад в г. Домодедово",
|
const normalizeInput = (value: string, previousValue: string) => {
|
||||||
"description": "Вы можете самостоятельно забрать заказ со склада.<div><br><div>Адрес: Московская обл, г. Домодедово, Каширское ш, 4, к.1, грузовая зона (заезд с ул. Пионерская).</div><div><br></div><div>График работы: пн-птн. 10.00-17.30</div><div><br></div><div><font color=\"#00a650\">Для юридических лиц необходима доверенность на получение груза.</font></div></div><br>",
|
// return nothing if no value
|
||||||
"active": "Y",
|
if (!value) return value;
|
||||||
"image": 0
|
|
||||||
|
// 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 MakeOrder = () => {
|
||||||
|
|
||||||
|
const qc = useQueryClient();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
|
||||||
const submitForm: SubmitHandler<FormDataValuesType> = async (data) => {
|
const submitForm: SubmitHandler<FormDataValuesType> = async (data) => {
|
||||||
setModalVisible(true)
|
setModalVisible(true)
|
||||||
const service = new localAPI()
|
const {data: responseData} = await axios.post('/api/v1/order/make', {
|
||||||
const responseData = await service.makeOrder({
|
fuserId: await getUserId(),
|
||||||
fuserId: await service.getFuserId(),
|
|
||||||
fullName: data.fullName,
|
fullName: data.fullName,
|
||||||
phoneNumber: data.phoneNumber.replace(/\D/g, ""),
|
email: data.email,
|
||||||
email: data.email
|
phoneNumber: data.phoneNumber
|
||||||
})
|
})
|
||||||
|
router.push(responseData.confirmation.confirmation_url)
|
||||||
router.push(responseData.data!.confirmation.confirmation_url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const takeawayInfo = useQuery({
|
const takeawayInfo = useQuery({
|
||||||
|
@ -52,33 +140,98 @@ const MakeOrder = () => {
|
||||||
queryFn: getTakeAwayInfo
|
queryFn: getTakeAwayInfo
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const {control, handleSubmit, formState: {errors}, watch, setValue} = useForm<FormDataValuesType>({
|
const {control, handleSubmit, formState: {errors}, watch, setValue} = useForm<FormDataValuesType>({
|
||||||
mode: "all",
|
mode: "all",
|
||||||
defaultValues: {}
|
defaultValues: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [phoneNumberPrev, setPhoneNumberPrev] = useState("")
|
||||||
|
const phoneNumberCur = watch("phoneNumber")
|
||||||
const receivingMethodCur = watch("receivingMethod")
|
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 [isDisabled, setIsDisabled] = useState(false)
|
||||||
|
|
||||||
const formId = nanoid(5)
|
const formId = nanoid(5)
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
return (
|
return (
|
||||||
<Wrapper title={"Оформление заказа"}
|
<Wrapper title={"Офромление заказа"}
|
||||||
breadcrumbs={[{name: "Корзина", link: "/cart"}, {name: "Оформление заказа", link: "/order/make"}]}>
|
breadcrumbs={[{name: "Корзина", link: "/cart"}, {name: "Оформление заказа", link: "/order/make"}]}>
|
||||||
<div className="grid grid-cols-10 gap-5 w-full pb-16 justify-between">
|
<div className="flex w-full pb-16 justify-between">
|
||||||
<form id={formId} className={"text-[#151515] [&>div]:mb-5 col-span-10 xl:col-span-7"}
|
<form id={formId} className={"text-[#151515] [&>div]:mb-5 w-8/12"} onSubmit={handleSubmit(submitForm)}>
|
||||||
onSubmit={handleSubmit(submitForm)}>
|
|
||||||
<div className="form__group bg-gray-card p-7 rounded-[30px]">
|
<div className="form__group bg-gray-card p-7 rounded-[30px]">
|
||||||
<h2 className={"mb-5 font-bold text-subtitle-2"}>Покупатель</h2>
|
<h2 className={"mb-5 font-bold text-subtitle-2"}>Покупатель</h2>
|
||||||
|
|
||||||
<div className="form__fields grid grid-cols-2 gap-5">
|
<div className="form__fields grid grid-cols-2 gap-5">
|
||||||
<FullNameInput errors={errors} control={control} watch={watch} setValue={setValue}/>
|
<Controller name={"fullName"} control={control}
|
||||||
<EmailInput errors={errors} control={control} watch={watch} setValue={setValue}/>
|
rules={{required: "Поле обязательно для заполнения"}}
|
||||||
<PhoneInput errors={errors} control={control} watch={watch} setValue={setValue}/>
|
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>
|
</div>
|
||||||
|
|
||||||
|
@ -87,11 +240,21 @@ const MakeOrder = () => {
|
||||||
className={"text-red-500"}>*</span></h2>
|
className={"text-red-500"}>*</span></h2>
|
||||||
|
|
||||||
<RadioGroup classNames={{"wrapper": cn("flex flex-row justify-center gap-10")}}
|
<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)
|
||||||
|
// }}
|
||||||
>
|
>
|
||||||
<DeliveryInput errors={errors} control={control} watch={watch} setValue={setValue}
|
|
||||||
deliveryType={"take-away"}/>
|
<Controller control={control} name={"receivingMethod"} render={({field}) =>
|
||||||
<DeliveryInput errors={errors} control={control} watch={watch} setValue={setValue}
|
<DeliveryTypeInput deliveryType={"take-away"} {...field} />} />
|
||||||
deliveryType={"delivery"}/>
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"receivingMethod"}
|
||||||
|
render={({field}) =>
|
||||||
|
<DeliveryTypeInput deliveryType={"delivery"} {...field} />} />
|
||||||
|
|
||||||
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
@ -115,37 +278,30 @@ const MakeOrder = () => {
|
||||||
<h2 className={"mb-5 font-bold text-subtitle-2"}>Адрес доставки</h2>
|
<h2 className={"mb-5 font-bold text-subtitle-2"}>Адрес доставки</h2>
|
||||||
|
|
||||||
<div className="form__fields grid grid-cols-2 gap-5">
|
<div className="form__fields grid grid-cols-2 gap-5">
|
||||||
<AddressInput errors={errors} control={control} watch={watch} setValue={setValue}/>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="form__group bg-gray-card p-7 rounded-[30px]">
|
<div className="form__group bg-gray-card p-7 rounded-[30px]">
|
||||||
<h2 className={"mb-5 font-bold text-subtitle-2"}>Служба доставки</h2>
|
<h2 className={"mb-5 font-bold text-subtitle-2"}>Служба доставки</h2>
|
||||||
{
|
<div className="form__fields">
|
||||||
<RadioGroup>
|
|
||||||
<Radio value={"cdek-pickup"}>
|
|
||||||
<span>Самовывоз из пункта выдачи СДЕК</span></Radio>
|
|
||||||
<Radio value={"cdek-delivery"}>Доставка курьером СДЕК</Radio>
|
|
||||||
</RadioGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group bg-gray-card p-7 rounded-[30px]">
|
|
||||||
<h2 className={"mb-5 font-bold text-subtitle-2"}>Самовывоз из пункта выдачи СДЕК</h2>
|
|
||||||
|
|
||||||
{
|
|
||||||
<CdekMap zoom={9} center={[55.784369, 37.711060]}
|
|
||||||
objects={[{name: "Дом", lat: 55.784369, lon: 37.711060, description: "Тестирование адреса пункта ПВЗ", pvzId: '1'}]}
|
|
||||||
watch={watch} setValue={setValue}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form__group bg-gray-card p-7 rounded-[30px]">
|
|
||||||
<h2 className={"mb-5 font-bold text-subtitle-2"}>Доставка курьером СДЕК</h2>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -158,6 +314,7 @@ const MakeOrder = () => {
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className={"flex flex-col gap-5"}
|
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)]"}>Оплата картами,
|
<Radio value={"online"} className={"flex-[1_1_calc((100%_/_2)_-_20px)]"}>Оплата картами,
|
||||||
SberPay, другие системы</Radio>
|
SberPay, другие системы</Radio>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
@ -176,13 +333,14 @@ const MakeOrder = () => {
|
||||||
Вас скоро перенаправит на страницу оплаты
|
Вас скоро перенаправит на страницу оплаты
|
||||||
</span>
|
</span>
|
||||||
<CircularProgress size={'lg'}/>
|
<CircularProgress size={'lg'}/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div className="flex flex-col col-span-10 xl:col-span-3">
|
<div className="flex flex-col w-[30%]">
|
||||||
<OrderInfo setIsDisabled={setIsDisabled} isDisabled={isDisabled}/>
|
<OrderInfo setIsDisabled={setIsDisabled} isDisabled={isDisabled}/>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Button type={"submit"} form={formId} color={"primary"}
|
<Button type={"submit"} form={formId} color={"primary"}
|
||||||
|
|
|
@ -1,70 +1,102 @@
|
||||||
import axios, {AxiosInstance} from "axios";
|
import axios, {AxiosInstance} from "axios";
|
||||||
import {
|
import {ResponseData} from "@/pages/api/v1/catalog";
|
||||||
CartItem,
|
|
||||||
Filter,
|
|
||||||
Fuser, GeocoderMetaData,
|
type createFUserType = {
|
||||||
GinGeoResponse,
|
fuserId: number
|
||||||
Invoice,
|
}
|
||||||
MakeOrderRequest,
|
|
||||||
News,
|
type getCartItemsType = {
|
||||||
Product, SdekPoint,
|
id: number,
|
||||||
Wrapper
|
code: string,
|
||||||
} from "@/service/types/local";
|
name: string,
|
||||||
|
is_active: number,
|
||||||
|
properties: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
quantity: number,
|
||||||
|
available_quantity: number
|
||||||
|
}
|
||||||
|
|
||||||
class LocalAPI {
|
class LocalAPI {
|
||||||
private instance: AxiosInstance;
|
private instance: AxiosInstance;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.instance = axios.create({
|
this.instance = axios.create({
|
||||||
baseURL: process.env.NODE_ENV === "development" ? "http://localhost:8000" : "https://relynolli.ru"
|
baseURL: process.env.NODE_ENV === "development" ? "http://localhost:8000": "https://relynolli.ru"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCatalogItems(filters: { [key: string]: string[] }, page: number = 1) {
|
async getCatalogItemsCount(){
|
||||||
|
const {data} = await this.instance.get<{status: number, info: string}>('/api/v1/catalog/count')
|
||||||
|
return +data.info
|
||||||
|
}
|
||||||
|
|
||||||
const {data} = await this.instance.get<Wrapper<Product[]>>('/api/v1/catalog', {
|
async getCatalogItems(filters: { [key: string]: string[] }, page: number = 1) {
|
||||||
params: {
|
const dataFilters: {[key: string]: string} = {}
|
||||||
limit: 10,
|
|
||||||
page,
|
for (const [key, value] of Object.entries(filters)) {
|
||||||
...filters
|
if (value.length === 0) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
dataFilters[key] = value.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(dataFilters).length === 0) {
|
||||||
|
const {data} = await this.instance.get('/api/v1/catalog', {
|
||||||
|
params: {
|
||||||
|
limit: 10,
|
||||||
|
page,
|
||||||
|
isFilter: 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
const {data} = await this.instance.get('/api/v1/catalog', {
|
||||||
|
params: {
|
||||||
|
limit: 10,
|
||||||
|
page,
|
||||||
|
isFilter: 1,
|
||||||
|
...dataFilters
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFilters() {
|
async getFilters() {
|
||||||
const {data} = await this.instance.get<Wrapper<Filter[]>>("/api/v1/catalog/filters")
|
const {data} = await this.instance.get("/api/v1/catalog/filters")
|
||||||
return data.data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCatalogItemByCode(code: string) {
|
async getCatalogItemByCode(code: string) {
|
||||||
const {data} = await this.instance.get<Wrapper<Product>>(`/api/v1/catalog/${code}`)
|
const {data} = await this.instance.get<ResponseData>(`/api/v1/catalog/${code}`)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFuserId() {
|
async getFuserId() {
|
||||||
let fuserId: string | number | null = localStorage.getItem('fuserId')
|
let fuserId: string | number | null = localStorage.getItem('fuserId')
|
||||||
if (fuserId === null || !Number.isInteger(+fuserId)) {
|
|
||||||
const data = await this.createFUser()
|
if (!fuserId) {
|
||||||
fuserId = data.fuserId
|
fuserId = (await this.createFUser()).fuserId
|
||||||
localStorage.setItem('fuserId', JSON.stringify(fuserId))
|
localStorage.setItem('fuserId', JSON.stringify(fuserId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("current fuserId", fuserId)
|
||||||
return +fuserId
|
return +fuserId
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFUser() {
|
async createFUser() {
|
||||||
const {data} = await this.instance.post<Wrapper<{ fuser: Fuser, fuserId: number }>>('/api/v1/cart')
|
const {data} = await this.instance.post<createFUserType>('/api/v1/cart')
|
||||||
console.log("Fuser id is", data)
|
console.log("Fuser id is", data)
|
||||||
return data.data!!
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCartItems() {
|
async getCartItems() {
|
||||||
|
|
||||||
const {data} = await this.instance.get<Wrapper<CartItem[]>>('/api/v1/cart', {
|
const {data} = await this.instance.get<getCartItemsType[]>('/api/v1/cart', {
|
||||||
params: {
|
params: {
|
||||||
fuserId: await this.getFuserId()
|
fuserId: await this.getFuserId()}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
@ -78,7 +110,8 @@ class LocalAPI {
|
||||||
try {
|
try {
|
||||||
const {data} = await this.instance.patch('/api/v1/cart/item', {productId, quantity, fuserId: await this.getFuserId()})
|
const {data} = await this.instance.patch('/api/v1/cart/item', {productId, quantity, fuserId: await this.getFuserId()})
|
||||||
return data
|
return data
|
||||||
} catch (e) {
|
}
|
||||||
|
catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,54 +122,8 @@ class LocalAPI {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async totalProductPrice(coupon: string | undefined) {
|
async totalProductPrice() {
|
||||||
if (coupon) {
|
const {data} = await this.instance.post('/api/v1/order/total', {fuserId: await this.getFuserId()})
|
||||||
const {data} = await this.instance.post<Wrapper<{ total: number, items: { cart: CartItem, discount: { name: string, value: number } }[] }>>('/api/v1/order/total', {fuserId: await this.getFuserId(), coupon})
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
const {data} = await this.instance.post<Wrapper<{ total: number, items: { cart: CartItem, discount: { name: string, value: number } }[] }>>('/api/v1/order/total', {fuserId: await this.getFuserId()})
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchNews(limit: number = 10, page: number = 1) {
|
|
||||||
const {data} = await this.instance.get<Wrapper<News[]>>("/api/v1/news")
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchArticles(limit: number = 10, page: number = 1) {
|
|
||||||
const {data} = await this.instance.get<Wrapper<News[]>>("/api/v1/articles")
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchAddresses(q: string | undefined) {
|
|
||||||
if (!q) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const {data} = await this.instance.get<Wrapper<GinGeoResponse[]>>("/api/v1/address/search", {
|
|
||||||
params: {q}
|
|
||||||
})
|
|
||||||
return data.data ? data.data : []
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchSdekPoints(lat: number, lon: number) {
|
|
||||||
const {data} = await this.instance.get<Wrapper<SdekPoint[]>>("/api/v1/cdek/points", {
|
|
||||||
params: {lat, lon}
|
|
||||||
})
|
|
||||||
return data.data? data.data: []
|
|
||||||
}
|
|
||||||
|
|
||||||
async retrieveNews(slug: string) {
|
|
||||||
const {data} = await this.instance.get<Wrapper<News>>("/api/v1/news/" + slug)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
async retrieveArticle(slug: string) {
|
|
||||||
const {data} = await this.instance.get<Wrapper<News>>("/api/v1/articles/" + slug)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
async makeOrder(req: MakeOrderRequest) {
|
|
||||||
const {data} = await this.instance.post<Wrapper<Invoice>>('/api/v1/order/make', req)
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,289 +0,0 @@
|
||||||
export interface Wrapper<T> {
|
|
||||||
status: string
|
|
||||||
data: T | null
|
|
||||||
meta: Meta
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CartItem {
|
|
||||||
id: number
|
|
||||||
fuserId: number
|
|
||||||
productId: number
|
|
||||||
priceTypeId: number
|
|
||||||
quantity: number
|
|
||||||
fuser: Fuser
|
|
||||||
product: Product
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Fuser {
|
|
||||||
id: number
|
|
||||||
code: string
|
|
||||||
userId: number
|
|
||||||
dateInserted: string
|
|
||||||
dateUpdated: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Product {
|
|
||||||
id: number
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
isActive: boolean
|
|
||||||
properties: Properties
|
|
||||||
detailText: string
|
|
||||||
price: Price
|
|
||||||
availableQuantity: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Properties {
|
|
||||||
acea?: string;
|
|
||||||
width?: string;
|
|
||||||
height?: string;
|
|
||||||
length?: string;
|
|
||||||
volume?: string;
|
|
||||||
weight?: string;
|
|
||||||
mileage?: string;
|
|
||||||
box_type?: string;
|
|
||||||
category?: string;
|
|
||||||
oil_type?: string;
|
|
||||||
documents?: string[];
|
|
||||||
use_areas?: string;
|
|
||||||
viscosity?: string;
|
|
||||||
acid_index?: string;
|
|
||||||
main_image?: string[];
|
|
||||||
pour_point?: string;
|
|
||||||
flash_point?: string;
|
|
||||||
subcategory?: string;
|
|
||||||
vendor_code?: string;
|
|
||||||
api_standart?: string;
|
|
||||||
requirements?: string;
|
|
||||||
viscosity_index?: string;
|
|
||||||
viscosity_kinematic?: string;
|
|
||||||
tribological_properties?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Price {
|
|
||||||
BASE: number
|
|
||||||
[key: string]: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Filter {
|
|
||||||
id: number
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
values: Value[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Value {
|
|
||||||
id: number
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Meta {
|
|
||||||
count?: number
|
|
||||||
page?: number
|
|
||||||
limit? : number
|
|
||||||
requestStarted: number
|
|
||||||
requestFinished: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface News {
|
|
||||||
id: number
|
|
||||||
isActive: boolean
|
|
||||||
sort: number
|
|
||||||
name: string
|
|
||||||
content: string
|
|
||||||
code: string
|
|
||||||
picture: string
|
|
||||||
date: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MakeOrderRequest {
|
|
||||||
fuserId: number
|
|
||||||
phoneNumber: string
|
|
||||||
fullName: string
|
|
||||||
email: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Invoice {
|
|
||||||
id: string
|
|
||||||
status: string
|
|
||||||
amount: Amount
|
|
||||||
description: string
|
|
||||||
recipient: Recipient
|
|
||||||
created_at: string
|
|
||||||
confirmation: Confirmation
|
|
||||||
paid: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Amount {
|
|
||||||
value: string
|
|
||||||
currency: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Recipient {
|
|
||||||
account_id: string
|
|
||||||
gateway_id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Confirmation {
|
|
||||||
type: string
|
|
||||||
confirmation_url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// GEOCODER API
|
|
||||||
export interface GinGeoResponse {
|
|
||||||
metaDataProperty: MetaDataProperty
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
boundedBy: BoundedBy
|
|
||||||
uri: string
|
|
||||||
Point: Point
|
|
||||||
}
|
|
||||||
export interface MetaDataProperty {
|
|
||||||
GeocoderMetaData: GeocoderMetaData
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GeocoderMetaData {
|
|
||||||
precision: string
|
|
||||||
text: string
|
|
||||||
kind: string
|
|
||||||
Address: Address
|
|
||||||
AddressDetails: AddressDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Address {
|
|
||||||
country_code: string
|
|
||||||
formatted: string
|
|
||||||
postal_code: string
|
|
||||||
Components: Component[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Component {
|
|
||||||
kind: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddressDetails {
|
|
||||||
Country: Country
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Country {
|
|
||||||
AddressLine: string
|
|
||||||
CountryNameCode: string
|
|
||||||
CountryName: string
|
|
||||||
AdministrativeArea: AdministrativeArea
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdministrativeArea {
|
|
||||||
AdministrativeAreaName: string
|
|
||||||
SubAdministrativeArea: SubAdministrativeArea
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubAdministrativeArea {
|
|
||||||
SubAdministrativeAreaName: string
|
|
||||||
Locality: Locality
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Locality {
|
|
||||||
LocalityName: string
|
|
||||||
Thoroughfare: Thoroughfare
|
|
||||||
DependentLocality: DependentLocality
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Thoroughfare {
|
|
||||||
ThoroughfareName: string
|
|
||||||
Premise: Premise
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Premise {
|
|
||||||
PremiseNumber: string
|
|
||||||
PostalCode: PostalCode
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PostalCode {
|
|
||||||
PostalCodeNumber: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DependentLocality {
|
|
||||||
DependentLocalityName: string
|
|
||||||
Thoroughfare: Thoroughfare2
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Thoroughfare2 {
|
|
||||||
ThoroughfareName: string
|
|
||||||
Premise: Premise2
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Premise2 {
|
|
||||||
PremiseNumber: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BoundedBy {
|
|
||||||
Envelope: Envelope
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Envelope {
|
|
||||||
lowerCorner: string
|
|
||||||
upperCorner: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Point {
|
|
||||||
pos: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SDEK
|
|
||||||
|
|
||||||
export interface SdekPoint {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
address_comment: string
|
|
||||||
work_time: string
|
|
||||||
phones: Phone[]
|
|
||||||
email: string
|
|
||||||
note: string
|
|
||||||
type: string
|
|
||||||
owner_code: string
|
|
||||||
take_only: boolean
|
|
||||||
is_handout: boolean
|
|
||||||
is_reception: boolean
|
|
||||||
is_dressing_room: boolean
|
|
||||||
have_cashless: boolean
|
|
||||||
have_cash: boolean
|
|
||||||
allowed_cod: boolean
|
|
||||||
work_time_list: WorkTimeList[]
|
|
||||||
weight_max: number
|
|
||||||
location: Location
|
|
||||||
fulfillment: boolean
|
|
||||||
nearest_station: string
|
|
||||||
nearest_metro_station: string
|
|
||||||
office_image_list: OfficeImageList[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Phone {
|
|
||||||
number: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkTimeList {
|
|
||||||
day: number
|
|
||||||
time: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Location {
|
|
||||||
country_code: string
|
|
||||||
region_code: number
|
|
||||||
region: string
|
|
||||||
city_code: number
|
|
||||||
city: string
|
|
||||||
fias_guid: string
|
|
||||||
postal_code: string
|
|
||||||
longitude: number
|
|
||||||
latitude: number
|
|
||||||
address: string
|
|
||||||
address_full: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OfficeImageList {
|
|
||||||
url: string
|
|
||||||
}
|
|
|
@ -12,16 +12,8 @@
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"typeRoots": [
|
|
||||||
"./types",
|
|
||||||
"./node_modules/@types",
|
|
||||||
"./node_modules/@yandex/ymaps3-types"
|
|
||||||
],
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"]
|
||||||
"ymaps3": [
|
|
||||||
"./node_modules/@yandex/ymaps3-types"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { YMap } from 'ymaps3';
|
|
||||||
|
|
||||||
declare let map: YMap;
|
|