Compare commits

..

No commits in common. "master" and "hotfix/hotfix-swiper" have entirely different histories.

41 changed files with 688 additions and 3378 deletions

View File

@ -1,6 +0,0 @@
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: 'https://relynolli.ru',
generateRobotsTxt: true, //
// ...other options
}

View File

@ -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: [
{ {
@ -33,14 +33,14 @@ const nextConfig = {
{ {
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. // Modify the file loader rule to ignore *.svg, since we have it handled now.
fileLoaderRule.exclude = /\.svg$/ fileLoaderRule.exclude = /\.svg$/i
return config return config
} }
}; };

View File

@ -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,14 +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", "swiper": "^11.0.7",
"valtio": "^1.13.0", "valtio": "^1.13.0",
"zod": "^3.22.4" "zod": "^3.22.4"
@ -43,7 +34,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",

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -1,9 +0,0 @@
# *
User-agent: *
Allow: /
# Host
Host: https://relynolli.ru
# Sitemaps
Sitemap: https://relynolli.ru/sitemap.xml

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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>
) )

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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> }

View File

@ -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

View File

@ -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>© ООО &quot;ТД Технохим Групп&quot; 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

View File

@ -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>
) )

View File

@ -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

View File

@ -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;

View File

@ -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>
) )
} }

View File

@ -1,36 +1,12 @@
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 />
<script
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> </body>
</Html> </Html>
); );

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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>

View File

@ -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({}, 1) 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.data! product: data
},
revalidate: 10 * 60
} }
} }
catch (e) {
return {
notFound: true,
revalidate: 10 * 60
}
}
} }

View File

@ -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 = (obj: { id: number, code: string, name: string, values: { id: number, value: string }[] } & {
dispatcher: SelectedFiltersDispatcher
}) => {
const CheckboxUI = (
obj: {
id: number;
code: string;
name: string;
values: { id: number; value: string }[];
} & {
dispatcher: SelectedFiltersDispatcher;
},
) => {
return ( return (
<Accordion> <Accordion>
<AccordionItem title={obj.name}> <AccordionItem title={obj.name}>
<CheckboxGroup> <CheckboxGroup>
{obj.values && {obj.values && obj.values.map(val => (
obj.values.map((val) => ( <Checkbox onChange={
<Checkbox () => {
onChange={() => {
obj.dispatcher((prevState) => { obj.dispatcher((prevState) => {
if (
prevState[obj.code] && if (prevState[obj.code] && prevState[obj.code].includes(val.value)) {
prevState[obj.code].includes(val.value)
) {
return { return {
...prevState, ...prevState,
[obj.code]: prevState[obj.code].filter( [obj.code]: prevState[obj.code].filter(v => v !== val.value)
(v) => v !== val.value, }
),
};
} else { } else {
return { return {
...prevState, ...prevState,
[obj.code]: [...(prevState[obj.code] || []), val.value], [obj.code]: [...(prevState[obj.code] || []), val.value]
};
} }
}); }
}} })
value={String(val.id)} }
key={"VALUE_" + val.id} } value={String(val.id)} key={"VALUE_" + val.id}>{val.value}</Checkbox>
>
{val.value}
</Checkbox>
))} ))}
</CheckboxGroup> </CheckboxGroup>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
); )
}; }
const FilterGenerator = ({ const FilterGenerator = ({
filterPropertiesData, filterPropertiesData,
setSelectedFilter, setSelectedFilter
}: Pick< }: Pick<InferGetStaticPropsType<typeof getStaticProps>, "filterPropertiesData"> & {
InferGetStaticPropsType<typeof getStaticProps>, setSelectedFilter: SelectedFiltersDispatcher
"filterPropertiesData"
> & {
setSelectedFilter: SelectedFiltersDispatcher;
}) => { }) => {
return ( return (
<Accordion <Accordion className={'filters mb-10 mr-4 lg:w-1/4 w-full'} defaultExpandedKeys={['1']}>
className={"filters mb-10 mr-4 w-full lg:w-1/4"} <AccordionItem title={"Фильтры"} key={"1"} aria-label={"Фильтры"}
defaultExpandedKeys={["1"]} classNames={{trigger: "bg-primary px-4", base: "border-1 rounded", title: "font-bold"}}>
> {filterPropertiesData.map(obj => (
<AccordionItem <CheckboxUI key={obj.id} {...obj} dispatcher={setSelectedFilter}/>
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> </AccordionItem>
</Accordion> </Accordion>
); )
}; }
const CatalogCard = (product: Product & { isFavourite: boolean }) => { type CatalogItemStruct = {
const isClient = useClient(); id: number
code: string,
name: string,
properties: {
[key: string]: string | string[]
}
detailText: string,
price: {
[key: string]: number | null
}
}
type CardProps = CatalogItemStruct & { isFavourite?: boolean }
const CatalogCard = (product: CardProps) => {
const isClient = useClient()
const cartItems = useQuery({ const cartItems = useQuery({
queryKey: ["cart"], queryKey: ['cart'], queryFn: async () => {
queryFn: async () => { const service = new LocalAPI()
const service = new LocalAPI(); return await service.getCartItems()
return await service.getCartItems(); }
}, })
});
const qs = useQueryClient();
const qs = useQueryClient()
const toggleCart = useMutation({ const toggleCart = useMutation({
mutationFn: async ({ mutationFn: async ({productId, quantity}: { productId: number, quantity: number }) => {
productId, const service = new LocalAPI()
quantity, if (cartItems.data!.find(({id}) => id === productId)) {
}: { return await service.deleteCartItem(productId)
productId: number;
quantity: number;
}) => {
const service = new LocalAPI();
if (
cartItems.data &&
cartItems.data.data &&
cartItems.data.data.find((item ) => item.product.id === productId)
) {
return await service.deleteCartItem(productId);
} }
return await service.addCartItem(productId, quantity); return await service.addCartItem(productId, quantity)
}, },
onSuccess: () => { onSuccess: () => {
qs.invalidateQueries({ queryKey: ["cart"] }); qs.invalidateQueries({queryKey: ["cart"]})
}, }
}); })
return ( return (
<div <div
className={ 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"}
"bg-gray-card h-fit min-h-[250px] w-full rounded-[20px] px-7 py-4 transition-shadow hover:cursor-pointer hover:shadow-md" 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/>}/>
} }
key={product.id} <div className="col-auto sm:col-start-4 sm:col-span-6 h-full flex flex-col justify-center">
>
<div
className={
"relative grid w-fit grid-cols-1 items-center sm:grid-cols-10"
}
>
{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 max-h-[250px]"}
loader={<Spinner />}
/>
)}
<div className="col-auto flex h-full flex-col justify-center sm:col-span-6 sm:col-start-4">
<span <span
className={"text-subtitle-4 mb-2 block font-normal text-[#52525C]"} 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>
Стандарт API: {product.properties.api_standart} Тип:{" "}
{product.properties.oil_type}
</span>
<h3 className={"text-black-3 text-lg font-bold uppercase"}>
{product.name}
</h3>
</div> </div>
{isClient ? ( {
<FavouriteIcon isClient ?
onClick={() => toggleFavourite(product.id)} <FavouriteIcon onClick={() => toggleFavourite(product.id)}
className={`absolute right-0 top-0 z-20 transition-colors ${product.isFavourite ? "fill-primary" : "fill-[#E0E3E3]"}`} className={`transition-colors absolute z-20 top-0 right-0 ${product.isFavourite ? "fill-primary" : "fill-[#E0E3E3]"}`}/> : null
/> }
) : 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"> <div
<span className="text-black-3 text-xl font-bold"> 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">
{`${product.price.BASE}`.replace(/\B(?=(\d{3})+(?!\d))/g, " ")} <span className="font-bold text-xl text-black-3">
{`${product.price.BASE}`.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')}
</span> </span>
{
{(cartItems.data) ? ( isClient ?
<Button <Button onClick={() => toggleCart.mutate({
onClick={() =>
toggleCart.mutate({
productId: product.id, productId: product.id,
quantity: 1, 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
} }
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 <Link href={'/catalog/' + product.code} className={'absolute top-0 left-0 z-10 w-full h-full'}/>
href={"/catalog/" + product.code}
className={"absolute left-0 top-0 z-10 h-full w-full"}
/>
</div> </div>
</div> </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 {favourites} = useSnapshot(favouritesStore)
{}, const [selectedFilters, setSelectedFilters] = useState<SelectedFiltersStruct>({})
);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [totalPageCount, setTotalPageCount] = useState(1); const [pageCount, setPageCount] = useState(1)
const [showPagination, setShowPagination] = useState(true);
useEffect(() => { useEffect(() => {
setPage(1); const service = new LocalAPI()
}, [selectedFilters]); service.getCatalogItemsCount().then(data => setPageCount(Math.ceil(data / 10)))
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 ( })
<Wrapper
title={"Каталог"} const catalogQuery = useQuery<CatalogItemStruct[]>({
breadcrumbs={[{ name: "Каталог", link: "/catalog" }]} queryKey: ["catalog", {selectedFilters, page}], queryFn: async () => {
> const service = new LocalAPI()
<div className="mb-4 flex justify-end"> return await service.getCatalogItems(selectedFilters, page)
<Pagination }
page={page} })
total={totalPageCount}
initialPage={1}
onChange={(page) => { return (<Wrapper title={"Каталог"} breadcrumbs={[{name: "Каталог", link: "/catalog"}]}>
setPage(page); <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>
<div className="flex flex-col justify-between lg:flex-row"> <div className="flex flex-col justify-between lg:flex-row">
<FilterGenerator <FilterGenerator filterPropertiesData={props.filterPropertiesData}
filterPropertiesData={props.filterPropertiesData} setSelectedFilter={setSelectedFilters}/>
setSelectedFilter={setSelectedFilters} <div className="products grid grid-cols-1 2xl:grid-cols-2 gap-7 lg:w-3/4 h-fit w-full">
/>
<div className="products grid h-fit w-full grid-cols-1 gap-7 lg:w-3/4 2xl:grid-cols-2">
{catalogQuery.isFetching && {catalogQuery.isFetching &&
Array(10) Array(10).fill(null).map((_, index) => <Skeleton key={index}
.fill(null) className={"h-[250px] rounded-[20px]"}/>)
.map((_, index) => ( }
<Skeleton key={index} className={"h-[250px] rounded-[20px]"} />
))}
{catalogQuery.data && {catalogQuery.data &&
catalogQuery.data.data!.map((product) => ( catalogQuery.data.map(product =>
<> <>
<CatalogCard <CatalogCard key={product.id} {...product}
key={product.id} isFavourite={favourites.includes(product.id)}/>
{...product}
isFavourite={favourites.includes(product.id)}
/>
</> </>
))} )
}
</div> </div>
</div> </div>
</Wrapper> </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,
}, }
}; }
}; }

View File

@ -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&amp;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;

View File

@ -1,4 +1,4 @@
import {Button, Divider, dropdownSection, Spinner, Tooltip} from "@nextui-org/react"; import {Button, dropdownSection, Tooltip} 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";
@ -9,12 +9,6 @@ import "swiper/css"
import 'swiper/css/navigation'; import 'swiper/css/navigation';
import 'swiper/css/pagination'; import 'swiper/css/pagination';
import "swiper/css/autoplay"; 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 (
@ -32,27 +26,24 @@ const Hero = () => {
} }
} }
navigation={ navigation={
{} {
} }
pagination={{clickable: true}} }
pagination={{ clickable: true }}
> >
<SwiperSlide> <SwiperSlide>
<section <section className={"w-full h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}>
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"}/> <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={"wrapper h-full flex flex-col justify-center relative z-20"}>
<div <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="w-11/12 mx-auto lg:m-0 xl:w-1/2 flex flex-col bg-black bg-opacity-25 py-4 xl:px-7 px-2 rounded-[20px]"> <h2 className={"text-2xl leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-8"}>Моторные масла для <span className={"text-primary"}>дизельных</span> двигателей</h2>
<h2 className={"text-xl uppercase leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-8"}>Моторные <span className={"text-sm xl:text-subtitle-1 leading-inherit text-subtitle-1 mb-4 xl:mb-6"}>Грузового транспорта и спецтехники</span>
масла для <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"} <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'}>
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>
<span <ChevronBannerIcon className={"stroke-[3px] stroke-black"} />
className={"text-sm text-[1.125rem] font-extrabold italic uppercase"}>Купить</span>
<ChevronBannerIcon className={"stroke-[3px] stroke-black"}/>
</Link> </Link>
</div> </div>
@ -60,21 +51,15 @@ const Hero = () => {
</section> </section>
</SwiperSlide> </SwiperSlide>
<SwiperSlide> <SwiperSlide>
<section <section className={"w-full bg-no-repeat h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}>
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"}/> <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={"wrapper h-full flex flex-col justify-center relative z-20"}>
<div <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="w-11/12 mx-auto lg:m-0 xl:w-1/2 flex flex-col bg-black bg-opacity-25 py-4 xl:px-7 px-2 rounded-[20px]"> <h2 className={"text-2xl leading-[100%] xl:text-title-4 2xl:text-title-3 mb-4 xl:mb-8"}><span className={"2xl:text-title-3 text-primary"}>Начать бизнес</span> по продажам качественного моторного масла </h2>
<h2 className={"text-[1.14rem] uppercase leading-[100%] xl:text-title-4 2xl:text-title-3 mb-4 xl:mb-8"}> <span className={"text-sm xl:text-title-2 leading-inherit text-subtitle-1 mb-4 xl:mb-6 text-primary"}>-ЛЕГКО</span>
<span className={"2xl:text-title-3 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'}>
качественного моторного масла </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> <span className={"text-sm text-[1.125rem] font-extrabold italic uppercase"}>Получить предложение о партнерстве</span>
<ChevronBannerIcon className={"stroke-[3px] stroke-black"}/> <ChevronBannerIcon className={"stroke-[3px] stroke-black"} />
</Link> </Link>
</div> </div>
@ -82,21 +67,16 @@ const Hero = () => {
</section> </section>
</SwiperSlide> </SwiperSlide>
<SwiperSlide key={Math.random()}> <SwiperSlide key={Math.random()}>
<section <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"}>
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"}/> <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={"wrapper h-full flex flex-col justify-center z-20 relative"}>
<div <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="w-11/12 mx-auto lg:m-0 xl:w-1/2 flex flex-col bg-black bg-opacity-25 py-4 xl:px-7 px-2 rounded-[20px]"> <h2 className={"text-2xl leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-8"}>Приглашаем к сотрудничеству СТО и автосервисы</h2>
<h2 className={"text-xl leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-8"}>Приглашаем <span className={"text-sm xl:text-subtitle-1 leading-inherit text-subtitle-1 mb-4 xl:mb-6"}>Поставляем масло <span className={"text-primary"}>RELYNOLLI ®</span> в партнерские СТО на льготных условиях</span>
к сотрудничеству СТО и автосервисы</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/"} <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'}>
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> <span className={"text-sm text-[1.125rem] font-extrabold italic uppercase"}>Узнать больше</span>
<ChevronBannerIcon className={"stroke-[3px] stroke-black"}/> <ChevronBannerIcon className={"stroke-[3px] stroke-black"} />
</Link> </Link>
</div> </div>
@ -108,97 +88,69 @@ const Hero = () => {
) )
} }
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={"hover:scale-105 relative group grayscale hover:grayscale-0 transition-all"}>
<Link href={"/catalog"} className={"absolute top-0 left-0 z-20 w-full h-full"}/> <Link href={"/catalog"} className={"absolute top-0 left-0 z-20 w-full h-full"}/>
<Image removeWrapper className={"w-full h-full object-cover brightness-75 relative z-10"} <Image removeWrapper className={"w-full h-full object-cover brightness-75 relative z-10"} src={"/oil2.png"}></Image>
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 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Масла для легковых автомобилей</h3>
для легковых автомобилей</h3>
<span <span className={"text-gray-2 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all delay-100"}>Серия 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"}> <Tooltip content={"Выпуск Апрель 2024"} className={"text-black"} size={"lg"}>
<Card className={"rounded-[30px] group transition-size"}> <Card className={"rounded-[30px] group transition-size grayscale"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50 grayscale"} <Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil1.png"}></Image>
src={"/oil1.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 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Специальная серия масел XMR</h3>
серия масел XMR</h3>
<span <span className={"text-gray-2 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all delay-100"}>Серия моторных масел 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>
<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> </CardHeader>
</Card> </Card>
</Tooltip> </Tooltip>
<Tooltip content={"Выпуск Июнь 2024"} className={"text-black"} size={"lg"}> <Tooltip content={"Выпуск Июнь 2024"} className={"text-black"} size={"lg"}>
<Card className={"rounded-[30px] transition-size group"}> <Card className={"rounded-[30px] transition-size group grayscale"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50 grayscale"} <Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil3.png"}></Image>
src={"/oil3.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 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Масла для коммерческого транспорта и спецтехники</h3>
для коммерческого транспорта и спецтехники</h3>
<span <span className={"text-gray-2 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all delay-100"}>Синтетические и полусинтетические масла для двигателей, которые эксплуатируются под высокими нагрузками. Подходят для бензиновых и дизельных двигателей</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> </CardHeader>
</Card> </Card>
</Tooltip> </Tooltip>
<Tooltip content={"Выпуск Апрель 2024"} className={"text-black"} size={"lg"}> <Tooltip content={"Выпуск Апрель 2024"} className={"text-black"} size={"lg"}>
<Card className={"rounded-[30px] transition-size group"}> <Card className={"rounded-[30px] transition-size group grayscale"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50 grayscale"} <Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil4.png"}></Image>
src={"/oil4.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 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Масла для мототехники</h3>
для мототехники</h3>
<span <span className={"text-gray-2 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all delay-100"}>Масла для четырех и двухтактных двигателей, которые помогут раскрыть неудержимый характер вашей мототехники</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> </CardHeader>
</Card> </Card>
</Tooltip> </Tooltip>
</div> </div>
@ -210,9 +162,7 @@ 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 transition-size"}>
@ -242,122 +192,28 @@ const Achievements = () => {
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/>
</> </>
); );
} }

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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"}

View File

@ -1,32 +1,63 @@
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) {
const dataFilters: {[key: string]: string} = {}
for (const [key, value] of Object.entries(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: { params: {
limit: 10, limit: 10,
page, page,
...filters isFilter: 0,
}
})
return data
}
const {data} = await this.instance.get('/api/v1/catalog', {
params: {
limit: 10,
page,
isFilter: 1,
...dataFilters
} }
}) })
@ -34,37 +65,38 @@ class LocalAPI {
} }
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
} }

View File

@ -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
}

View File

@ -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"],

3
types/index.d.ts vendored
View File

@ -1,3 +0,0 @@
import { YMap } from 'ymaps3';
declare let map: YMap;

753
yarn.lock

File diff suppressed because it is too large Load Diff