update templates

master
Ernest Litvinenko 2024-03-28 18:19:18 +03:00
parent 42a36cab93
commit b94292b479
16 changed files with 775 additions and 392 deletions

BIN
public/ozon_icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
public/ozon_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

21
public/tg_icon.svg Normal file
View File

@ -0,0 +1,21 @@
<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>

After

Width:  |  Height:  |  Size: 1.5 KiB

21
public/vk_icon.svg Normal file
View File

@ -0,0 +1,21 @@
<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>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -23,7 +23,7 @@ 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>
<span>{String(totalProductPriceQs.data?.total_product_price).replace(/\B(?=(\d{3})+(?!\d))/g, " ")} </span> <span>{String(totalProductPriceQs.data ? totalProductPriceQs.data.data!.total : 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 type="text" label="Введите промокод" variant={"bordered"} <Input type="text" label="Введите промокод" variant={"bordered"}
@ -43,12 +43,12 @@ const OrderInfo = (props: OrderInfoProps) => {
<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?.total_product_price).replace(/\B(?=(\d{3})+(?!\d))/g, " ")} </span> <span className={"text-title-3 font-bold"}>{String(totalProductPriceQs.data ? totalProductPriceQs.data.data!.total : 0).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?.total_product_price * 0.2} </span> <span>{totalProductPriceQs.data ? totalProductPriceQs.data.data!.total * 20 / 120 : 0} </span>
</div> </div>
<Checkbox isSelected={!props.isDisabled} onChange={() => { <Checkbox isSelected={!props.isDisabled} onChange={() => {

View File

@ -0,0 +1,67 @@
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 {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="#" className={"group"}><VkIcon className={"fill-gray-3 group-hover:fill-primary transition-colors [&_path]:fill-white"}/></a>
<a href="#" className={"group"}><TgIcon className={"fill-gray-3 group-hover:fill-primary transition-colors [&_path]:fill-white"} /></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"}>Новости</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"}>Продукция</h2>
<ul className={"text-gray-3 text-sm font-semibold my-2"}>
<li className={"hover:text-primary transition-colors"}>Relynolli ® Standart M</li>
<li className={"hover:text-primary transition-colors"}>Relynolli ® Premium M</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"}>Контакты</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"}>Политика конфиденциальности</p>
<p className={"text-gray-3 text-sm font-semibold my-2"}>Обработка персональных данных</p>
</div>
</div>
</footer>
)
}
export default Footer

View File

@ -136,7 +136,7 @@ const Header = () => {
</div> </div>
{cart.data && {cart.data &&
<Badge isInvisible={cart.data?.length === 0} color={"primary"} content={cart.data?.length} <Badge isInvisible={!Boolean(cart.data.data)} color={"primary"} content={cart.data.data && cart.data.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]"}>
@ -198,7 +198,7 @@ const Header = () => {
</div> </div>
{cart.data && {cart.data &&
<Badge isInvisible={cart.data?.length === 0} color={"primary"} content={cart.data?.length} <Badge isInvisible={!Boolean(cart.data.data)} color={"primary"} content={cart.data.data && cart.data.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]"}>
@ -206,15 +206,9 @@ const Header = () => {
</Link> </Link>
</Badge> </Badge>
} }
</div> </div>
</div> </div>
</div> </div>
</header> </header>
) )

View File

@ -1,5 +1,6 @@
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";
const mulish = Mulish({ const mulish = Mulish({
subsets: ["cyrillic", "latin"], subsets: ["cyrillic", "latin"],
@ -12,6 +13,7 @@ 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

@ -16,7 +16,7 @@ type WrapperProps = {
const Wrapper = (props: WrapperProps) => { const Wrapper = (props: WrapperProps) => {
return <> return <>
<section className={"bg-white text-black pt-7"}> <section className={"bg-white text-black py-7"}>
<div className="wrapper"> <div className="wrapper">
{ {
props.breadcrumbs && props.breadcrumbs &&

View File

@ -1,8 +1,6 @@
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"
@ -15,9 +13,11 @@ 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 = (product: Partial<ResponseData> & { quantity: number, available_quantity: number }) => { const CartCard = (item: CartItem) => {
const qc = useQueryClient() const qc = useQueryClient()
@ -36,39 +36,39 @@ const CartCard = (product: Partial<ResponseData> & { quantity: number, available
return ( return (
<div <div
className="card relative flex bg-gray-card rounded-[20px] w-full px-7 py-4 group hover:shadow-md transition-shadow cursor-pointer justify-between items-center text-[#151515] [&>*]:mr-7"> 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">
<Image className={"grow"} src={`https://tehnohimgrupp.ru/upload/${product.properties!.main_image![0]}`} <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"}/>
width={100} height={126} alt={"oil"}/> <div className="col-span-10 xl:col-span-3 text-block flex flex-col justify-between font-bold text-xl xl:text-base">
<div className="text-block flex flex-col h-1/2 justify-between py-5 w-1/3"> <p>{item.product.name}</p>
<p>{product.name}</p> <p className={"text-[#8F8F8F] font-normal pt-2"}>Артикул: {item.product.properties.vendor_code}</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={"grow"}>{`${+(product.properties!.weight!) / 1000} кг`}</span> <span className={"col-span-3 xl:col-span-1"}>{item.product.properties!.volume}</span>
<span className={"grow"}>{product.properties!.volume}</span> <div className="flex col-start-8 col-span-3 xl:col-span-2 justify-between relative z-20">
<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>{product.quantity}</span> <span>{item.quantity}</span>
<button onClick={() => changeQuantity.mutate({productId: product.id!, quantity: product.quantity + 1})} <button onClick={() => changeQuantity.mutate({productId: item.product.id!, quantity: item.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 grow text-center"> <div className="flex flex-col font-bold text-right text-3xl col-span-10 border-t-1 border-t-primary pt-4">
{product.price!.BASE} {item.product.price!.BASE}
</div> </div>
<button className={"relative z-20"} onClick={() => toggleCart(product as ResponseData)}> <button className={"z-20 absolute top-8 right-0"} onClick={() => {
// TODO DO
}}>
<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/" + product.code} className={"absolute top-0 left-0 w-full h-full z-10"}></Link> <Link href={"/catalog/" + item.product.code} className={"absolute top-0 left-0 w-full h-full z-10"}></Link>
</div> </div>
) )
} }
@ -77,7 +77,7 @@ const PlaceHolder = () => {
return ( return (
<div <div
className="card relative flex flex-col bg-gray-card rounded-[20px] w-full px-7 py-4 justify-between items-center text-[#151515] [&>*]:mr-7 text-subtitle-3"> 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">
<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>
@ -108,17 +108,16 @@ const Cart = () => {
return ( return (
<Wrapper title={"Корзина"} breadcrumbs={[{name: "Корзина", link: "/cart"}]}> <Wrapper title={"Корзина"} breadcrumbs={[{name: "Корзина", link: "/cart"}]}>
<div className="flex w-full pb-16 justify-between"> <div className="grid grid-cols-10 pb-16 justify-between gap-5">
{ {(cart.data && cart.data.data) ? (
cart.data?.length ? (
<> <>
<div className="cards w-8/12 [&>*]:mb-7"> <div className="cards order-2 col-span-10 xl:order-none xl:col-span-7 flex-col">
{cart.data.map(item => <CartCard key={item.id} {...item}/>)} {cart.data.data.map(item => <CartCard key={item.id} {...item} />)}
</div> </div>
<div className="flex flex-col w-[30%]"> <div className="flex order-1 col-span-10 xl:order-none xl:col-span-3 flex-col">
<OrderInfo <OrderInfo
setIsDisabled={setIsDisabled} isDisabled={isDisabled}/> setIsDisabled={setIsDisabled} isDisabled={isDisabled}/>

View File

@ -1,8 +1,6 @@
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"
@ -12,10 +10,11 @@ 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<any[]>({ const cartItems = useQuery<Wrapper<CartItem[]>>({
queryKey: ['cart'] queryKey: ['cart']
}) })
const isClient = useClient() const isClient = useClient()
@ -25,7 +24,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!.find(({id}) => id === productId)) { if (cartItems.data && cartItems.data.data && cartItems.data.data.find(item => item.product.id === productId)) {
return await service.deleteCartItem(productId) return await service.deleteCartItem(productId)
} }
return await service.addCartItem(productId, quantity) return await service.addCartItem(productId, quantity)
@ -85,7 +84,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.map(({id})=> id).includes(product.id) ? "В корзине" : "Добавить в корзину"} {(cartItems.data && cartItems.data.data && cartItems.data.data.map((item)=> item.product.id).includes(product.id)) ? "В корзине" : "Добавить в корзину"}
</Button>} </Button>}
{ {
@ -181,8 +180,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) as {code: string}[] const {data} = await service.getCatalogItems({}, 1)
const codes = data.map(item => ({params: {code: item.code}})) const codes = data!.map(item => ({params: {code: item.code}}))
return { return {
paths: [ paths: [
...codes ...codes
@ -194,10 +193,20 @@ export const getStaticPaths = async () => {
export const getStaticProps = async ({params: {code}}: { params: { code: string } }) => { export const getStaticProps = async ({params: {code}}: { params: { code: string } }) => {
const service = new LocalAPI() const service = new LocalAPI()
try {
const data = await service.getCatalogItemByCode(code) const data = await service.getCatalogItemByCode(code)
return { return {
props: { props: {
product: data product: data.data!
},
revalidate: 15*60
} }
} }
catch (e) {
return {
redirect: {"destination": "/404", "permanent": false},
revalidate: 15*60
}
}
} }

View File

@ -1,237 +1,304 @@
import { import {
CheckboxGroup, CheckboxGroup,
Checkbox, Checkbox,
Button, Skeleton, Accordion, AccordionItem, Pagination, Spinner Button,
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 {element} from "prop-types"; import { Product } from "@/service/types/local";
type SelectedFiltersStruct = { type SelectedFiltersStruct = {
[key: string]: string[] [key: string]: string[];
} };
type SelectedFiltersDispatcher = Dispatch<SetStateAction<SelectedFiltersStruct>> type SelectedFiltersDispatcher = Dispatch<
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.map(val => ( {obj.values &&
<Checkbox onChange={ obj.values.map((val) => (
() => { <Checkbox
onChange={() => {
obj.dispatcher((prevState) => { obj.dispatcher((prevState) => {
if (
if (prevState[obj.code] && prevState[obj.code].includes(val.value)) { prevState[obj.code] &&
prevState[obj.code].includes(val.value)
) {
return { return {
...prevState, ...prevState,
[obj.code]: prevState[obj.code].filter(v => v !== val.value) [obj.code]: prevState[obj.code].filter(
} (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)}
} value={String(val.id)} key={"VALUE_" + val.id}>{val.value}</Checkbox> key={"VALUE_" + val.id}
>
{val.value}
</Checkbox>
))} ))}
</CheckboxGroup> </CheckboxGroup>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
) );
} };
const FilterGenerator = ({ const FilterGenerator = ({
filterPropertiesData, filterPropertiesData,
setSelectedFilter setSelectedFilter,
}: Pick<InferGetStaticPropsType<typeof getStaticProps>, "filterPropertiesData"> & { }: Pick<
setSelectedFilter: SelectedFiltersDispatcher InferGetStaticPropsType<typeof getStaticProps>,
"filterPropertiesData"
> & {
setSelectedFilter: SelectedFiltersDispatcher;
}) => { }) => {
return ( return (
<Accordion className={'filters mb-10 mr-4 lg:w-1/4 w-full'} defaultExpandedKeys={['1']}> <Accordion
<AccordionItem title={"Фильтры"} key={"1"} aria-label={"Фильтры"} className={"filters mb-10 mr-4 w-full lg:w-1/4"}
classNames={{trigger: "bg-primary px-4", base: "border-1 rounded", title: "font-bold"}}> defaultExpandedKeys={["1"]}
{filterPropertiesData.map(obj => ( >
<CheckboxUI key={obj.id} {...obj} dispatcher={setSelectedFilter}/> <AccordionItem
title={"Фильтры"}
key={"1"}
aria-label={"Фильтры"}
classNames={{
trigger: "bg-primary px-4",
base: "border-1 rounded",
title: "font-bold",
}}
>
{filterPropertiesData.map((obj) => (
<CheckboxUI key={obj.id} {...obj} dispatcher={setSelectedFilter} />
))} ))}
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
) );
} };
type CatalogItemStruct = { const CatalogCard = (product: Product & { isFavourite: boolean }) => {
id: number const isClient = useClient();
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'], queryFn: async () => { queryKey: ["cart"],
const service = new LocalAPI() queryFn: async () => {
return await service.getCartItems() const service = new LocalAPI();
} return await service.getCartItems();
}) },
});
const qs = useQueryClient();
const qs = useQueryClient()
const toggleCart = useMutation({ const toggleCart = useMutation({
mutationFn: async ({productId, quantity}: { productId: number, quantity: number }) => { mutationFn: async ({
const service = new LocalAPI() productId,
if (cartItems.data!.find(({id}) => id === productId)) { quantity,
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={"bg-gray-card w-full h-fit min-h-[250px] py-4 px-7 rounded-[20px] hover:shadow-md transition-shadow hover:cursor-pointer"} className={
key={product.id}> "bg-gray-card h-fit min-h-[250px] w-full rounded-[20px] px-7 py-4 transition-shadow hover:cursor-pointer hover:shadow-md"
<div className={"grid grid-cols-1 sm:grid-cols-10 w-fit relative items-center"}>
{
isClient && product.properties.main_image &&
<Img src={`https://relynolli.ru/upload/${product.properties.main_image[0]}`} alt={product.name}
className={"col-auto mx-auto mb-4 sm:col-span-2 sm:row-span-2"}
loader={<Spinner/>}/>
} }
<div className="col-auto sm:col-start-4 sm:col-span-6 h-full flex flex-col justify-center"> key={product.id}
<span >
className={"text-[#52525C] font-normal text-subtitle-4 mb-2 block"}>Стандарт API: {product.properties.api_standart} Тип: {product.properties.oil_type}</span>
<h3 className={"font-bold text-lg uppercase text-black-3"}>{product.name}</h3>
</div>
{
isClient ?
<FavouriteIcon onClick={() => toggleFavourite(product.id)}
className={`transition-colors absolute z-20 top-0 right-0 ${product.isFavourite ? "fill-primary" : "fill-[#E0E3E3]"}`}/> : null
}
<div <div
className="flex col-auto row-auto sm:row-start-2 sm:col-start-4 sm:col-span-7 justify-between w-full items-center pt-4"> className={
<span className="font-bold text-xl text-black-3"> "relative grid w-fit grid-cols-1 items-center sm:grid-cols-10"
{`${product.price.BASE}`.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')}
</span>
{
isClient ?
<Button onClick={() => toggleCart.mutate({
productId: product.id,
quantity: 1
})}
className={"font-bold text-lg bg-green-2 uppercase italic text-black-3 relative z-20"}>{cartItems.data?.map(({id}) => id).includes(product.id) ? "В корзине" : "В корзину"}</Button> : null
} }
>
{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
className={"text-subtitle-4 mb-2 block font-normal text-[#52525C]"}
>
Стандарт API: {product.properties.api_standart} Тип:{" "}
{product.properties.oil_type}
</span>
<h3 className={"text-black-3 text-lg font-bold uppercase"}>
{product.name}
</h3>
</div>
{isClient ? (
<FavouriteIcon
onClick={() => toggleFavourite(product.id)}
className={`absolute right-0 top-0 z-20 transition-colors ${product.isFavourite ? "fill-primary" : "fill-[#E0E3E3]"}`}
/>
) : null}
<Link href={'/catalog/' + product.code} className={'absolute top-0 left-0 z-10 w-full h-full'}/> <div className="col-auto row-auto flex w-full items-center justify-between pt-4 sm:col-span-7 sm:col-start-4 sm:row-start-2">
<span className="text-black-3 text-xl font-bold">
{`${product.price.BASE}`.replace(/\B(?=(\d{3})+(?!\d))/g, " ")}
</span>
{(cartItems.data) ? (
<Button
onClick={() =>
toggleCart.mutate({
productId: product.id,
quantity: 1,
})
}
className={
"bg-green-2 text-black-3 relative z-20 text-lg font-bold uppercase italic"
}
>
{(cartItems.data && cartItems.data.data && cartItems.data.data
.map(({ productId }) => productId)
.includes(product.id))
? "В корзине"
: "В корзину"}
</Button>
) : null}
<Link
href={"/catalog/" + product.code}
className={"absolute left-0 top-0 z-10 h-full w-full"}
/>
</div> </div>
</div> </div>
</div> </div>
) );
} };
const Catalog = (props: InferGetStaticPropsType<typeof getStaticProps>) => { const Catalog = (props: InferGetStaticPropsType<typeof getStaticProps>) => {
const { favourites } = useSnapshot(favouritesStore);
const {favourites} = useSnapshot(favouritesStore) const [selectedFilters, setSelectedFilters] = useState<SelectedFiltersStruct>(
const [selectedFilters, setSelectedFilters] = useState<SelectedFiltersStruct>({}) {},
);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageCount, setPageCount] = useState(1) const [totalPageCount, setTotalPageCount] = useState(1);
const [showPagination, setShowPagination] = useState(true);
useEffect(() => { useEffect(() => {
const service = new LocalAPI() setPage(1);
service.getCatalogItemsCount().then(data => setPageCount(Math.ceil(data / 10))) }, [selectedFilters]);
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
const catalogQuery = useQuery<CatalogItemStruct[]>({ title={"Каталог"}
queryKey: ["catalog", {selectedFilters, page}], queryFn: async () => { breadcrumbs={[{ name: "Каталог", link: "/catalog" }]}
const service = new LocalAPI() >
return await service.getCatalogItems(selectedFilters, page) <div className="mb-4 flex justify-end">
} <Pagination
}) page={page}
total={totalPageCount}
initialPage={1}
return (<Wrapper title={"Каталог"} breadcrumbs={[{name: "Каталог", link: "/catalog"}]}> onChange={(page) => {
<div className="flex justify-end mb-4"> setPage(page);
{ !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 filterPropertiesData={props.filterPropertiesData} <FilterGenerator
setSelectedFilter={setSelectedFilters}/> filterPropertiesData={props.filterPropertiesData}
<div className="products grid grid-cols-1 2xl:grid-cols-2 gap-7 lg:w-3/4 h-fit w-full"> setSelectedFilter={setSelectedFilters}
/>
<div className="products grid h-fit w-full grid-cols-1 gap-7 lg:w-3/4 2xl:grid-cols-2">
{catalogQuery.isFetching && {catalogQuery.isFetching &&
Array(10).fill(null).map((_, index) => <Skeleton key={index} Array(10)
className={"h-[250px] rounded-[20px]"}/>) .fill(null)
} .map((_, index) => (
<Skeleton key={index} className={"h-[250px] rounded-[20px]"} />
))}
{catalogQuery.data && {catalogQuery.data &&
catalogQuery.data.map(product => catalogQuery.data.data!.map((product) => (
<> <>
<CatalogCard key={product.id} {...product} <CatalogCard
isFavourite={favourites.includes(product.id)}/> key={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,4 +1,4 @@
import {Button, dropdownSection, Tooltip} from "@nextui-org/react"; import {Button, Divider, dropdownSection, Spinner, 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,6 +9,12 @@ 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 (
@ -26,24 +32,27 @@ const Hero = () => {
} }
} }
navigation={ navigation={
{ {}
} }
} pagination={{clickable: true}}
pagination={{ clickable: true }}
> >
<SwiperSlide> <SwiperSlide>
<section className={"w-full h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}> <section
className={"w-full h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}>
<img src={"/cars.PNG"} className={"object-cover h-full w-full z-10 absolute"} alt={"cars"}/> <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 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]"> <div
<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> 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>
<span className={"text-sm xl:text-subtitle-1 leading-inherit text-subtitle-1 mb-4 xl:mb-6"}>Грузового транспорта и спецтехники</span> <span className={"text-sm xl:text-subtitle-1 leading-inherit text-subtitle-1 mb-4 xl:mb-6"}>Грузового транспорта и спецтехники</span>
<Link href={"/catalog"} className={'w-full lg:w-1/2 bg-green-2 rounded-[8px] p-6 flex justify-between items-center text-black hover:scale-105 transition'}> <Link href={"/catalog"}
<span className={"text-sm text-[1.125rem] font-extrabold italic uppercase"}>Купить</span> 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'}>
<ChevronBannerIcon className={"stroke-[3px] stroke-black"} /> <span
className={"text-sm text-[1.125rem] font-extrabold italic uppercase"}>Купить</span>
<ChevronBannerIcon className={"stroke-[3px] stroke-black"}/>
</Link> </Link>
</div> </div>
@ -51,15 +60,21 @@ const Hero = () => {
</section> </section>
</SwiperSlide> </SwiperSlide>
<SwiperSlide> <SwiperSlide>
<section className={"w-full bg-no-repeat h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}> <section
className={"w-full bg-no-repeat h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}>
<img src={"/hands.PNG"} className={"object-cover h-full w-full z-10 absolute"} alt={"hands"}/> <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 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]"> <div
<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> 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]">
<span className={"text-sm xl:text-title-2 leading-inherit text-subtitle-1 mb-4 xl:mb-6 text-primary"}>-ЛЕГКО</span> <h2 className={"text-2xl leading-[100%] xl:text-title-4 2xl:text-title-3 mb-4 xl:mb-8"}>
<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={"2xl:text-title-3 text-primary"}>Начать бизнес</span> по продажам
качественного моторного масла </h2>
<span
className={"text-sm 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>
@ -67,16 +82,21 @@ const Hero = () => {
</section> </section>
</SwiperSlide> </SwiperSlide>
<SwiperSlide key={Math.random()}> <SwiperSlide key={Math.random()}>
<section className={"w-full bg-cars bg-no-repeat h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}> <section
className={"w-full bg-cars bg-no-repeat h-[796px] mb-2 text-white font-bold bg-cover bg-center xl:bg-right relative"}>
<img src={"/sto.PNG"} className={"object-cover h-full w-full z-10 absolute"} alt={"auto services"}/> <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 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]"> <div
<h2 className={"text-2xl leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-8"}>Приглашаем к сотрудничеству СТО и автосервисы</h2> 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]">
<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 className={"text-2xl leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-8"}>Приглашаем
к сотрудничеству СТО и автосервисы</h2>
<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>
<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'}> <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>
@ -88,69 +108,97 @@ 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"}>Моторные масла и смазочные материалы <br/> <h1 className={"text-4xl xl:text-title-1 leading-normal font-bold text-center italic uppercase text-black mb-8"}>Моторные
масла и смазочные материалы <br/>
<span className={"text-primary"}>Relynolli ®</span></h1> <span className={"text-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 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%]"> <div
className="wrapper mx-auto py-8 grid grid-cols-1 lg:grid-cols-2 grid-rows-3 lg:grid-rows-2 gap-4 h-auto [&>*]:h-[390px] [&_span]:text-base [&_span]:md:text-xl [&_span]:leading-[110%]">
<Card className={"hover:scale-105 relative group grayscale hover:grayscale-0 transition-all"}> <Card className={"lg:hover:scale-105 relative group lg: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"} src={"/oil2.png"}></Image> <Image removeWrapper className={"w-full h-full object-cover brightness-75 relative z-10"}
src={"/oil2.png"}></Image>
<CardHeader <CardHeader
className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between"> className="absolute z-10 top-10 left-10 bottom-5 flex-col !items-start w-3/4 justify-between">
<h3 className={"text-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 className={"text-3xl lg:text-title-3 leading-[100%] text-white lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Масла
для легковых автомобилей</h3>
<span className={"text-gray-2 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all delay-100"}>Серия Standart и Premium для легковых автомобилей для наилучшей энергоэффективности двигателя вашего автомобиля</span> <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 grayscale"}> <Card className={"rounded-[30px] group transition-size"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil1.png"}></Image> <Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50 grayscale"}
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-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> <h3 className={"text-3xl lg:text-title-3 leading-[100%] text-white lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Специальная
серия масел XMR</h3>
<span 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> <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 grayscale"}> <Card className={"rounded-[30px] transition-size group"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil3.png"}></Image> <Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50 grayscale"}
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-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 className={"text-3xl lg:text-title-3 leading-[100%] text-white lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Масла
для коммерческого транспорта и спецтехники</h3>
<span className={"text-gray-2 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all delay-100"}>Синтетические и полусинтетические масла для двигателей, которые эксплуатируются под высокими нагрузками. Подходят для бензиновых и дизельных двигателей</span> <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 grayscale"}> <Card className={"rounded-[30px] transition-size group"}>
<Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50"} src={"/oil4.png"}></Image> <Image removeWrapper className={"z-0 w-full h-full object-cover brightness-50 grayscale"}
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-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 className={"text-3xl lg:text-title-3 leading-[100%] text-white lg:opacity-0 lg:translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"}>Масла
для мототехники</h3>
<span className={"text-gray-2 opacity-0 translate-y-1/2 group-hover:opacity-100 group-hover:translate-y-0 transition-all delay-100"}>Масла для четырех и двухтактных двигателей, которые помогут раскрыть неудержимый характер вашей мототехники</span> <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>
@ -162,7 +210,9 @@ const Achievements = () => {
return ( return (
<section className={"bg-cover backdrop-brightness-50 relative"}> <section className={"bg-cover backdrop-brightness-50 relative"}>
<Image removeWrapper alt={"oiltypeImage"} className={"absolute top-0 left-0 w-full h-full brightness-50 object-fill -z-10"} src={"/oiltypeImage.png"}></Image> <Image removeWrapper alt={"oiltypeImage"}
className={"absolute top-0 left-0 w-full h-full brightness-50 object-fill -z-10"}
src={"/oiltypeImage.png"}></Image>
<div className="wrapper py-8 mb-8 flex flex-col justify-center"> <div className="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"}>
@ -192,28 +242,122 @@ 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 "}>Моторные масла Relynolli ®</h3> <h3 className={"text-2xl md:text-3xl leading-[35px] text-white mb-6 "}>Моторные масла
Relynolli ®</h3>
<span className={"text-base md:text-xl text-white opacity-50"}>Обладают высокой смазывающей способностью и обеспечивают надёжную защиту двигателя от износа</span> <span className={"text-base md:text-xl text-white opacity-50"}>Обладают высокой смазывающей способностью и обеспечивают надёжную защиту двигателя от износа</span>
</div> </div>
<Button className={"bg-green-2 font-bold uppercase italic"} endContent={<ChevronBannerIcon <Button className={"bg-green-2 font-bold uppercase italic w-[200px] h-fit text-wrap"}
className={"stroke-[3px] stroke-black"}/>}>Расшифровка масел группы - N</Button> endContent={<ChevronBannerIcon
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>
<Button className={"bg-green-2 font-bold uppercase italic mx-auto"} endContent={<ChevronBannerIcon className={"stroke-[3px] stroke-black"}/>}>Перейти в продукцию</Button> <Link className={"mx-auto"} href={"/catalog"}><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={"/brand/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

@ -163,8 +163,8 @@ const MakeOrder = () => {
return ( return (
<Wrapper title={"Офромление заказа"} <Wrapper title={"Офромление заказа"}
breadcrumbs={[{name: "Корзина", link: "/cart"}, {name: "Оформление заказа", link: "/order/make"}]}> breadcrumbs={[{name: "Корзина", link: "/cart"}, {name: "Оформление заказа", link: "/order/make"}]}>
<div className="flex w-full pb-16 justify-between"> <div className="grid grid-cols-10 gap-5 w-full pb-16 justify-between">
<form id={formId} className={"text-[#151515] [&>div]:mb-5 w-8/12"} onSubmit={handleSubmit(submitForm)}> <form id={formId} className={"text-[#151515] [&>div]:mb-5 col-span-10 xl:col-span-7"} 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>
@ -340,7 +340,7 @@ const MakeOrder = () => {
</ModalContent> </ModalContent>
</Modal> </Modal>
<div className="flex flex-col w-[30%]"> <div className="flex flex-col col-span-10 xl:col-span-3">
<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,22 +1,5 @@
import axios, {AxiosInstance} from "axios"; import axios, {AxiosInstance} from "axios";
import {ResponseData} from "@/pages/api/v1/catalog"; import {CartItem, Filter, Fuser, News, Product, Wrapper} from "@/service/types/local";
type createFUserType = {
fuserId: number
}
type getCartItemsType = {
id: number,
code: string,
name: string,
is_active: number,
properties: {
[key: string]: string
}
quantity: number,
available_quantity: number
}
class LocalAPI { class LocalAPI {
private instance: AxiosInstance; private instance: AxiosInstance;
@ -27,37 +10,13 @@ class LocalAPI {
}) })
} }
async getCatalogItemsCount(){
const {data} = await this.instance.get<{status: number, info: string}>('/api/v1/catalog/count')
return +data.info
}
async getCatalogItems(filters: { [key: string]: string[] }, page: number = 1) { async getCatalogItems(filters: { [key: string]: string[] }, page: number = 1) {
const dataFilters: {[key: string]: string} = {}
for (const [key, value] of Object.entries(filters)) { const {data} = await this.instance.get<Wrapper<Product[]>>('/api/v1/catalog', {
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,
isFilter: 0, ...filters
}
})
return data
}
const {data} = await this.instance.get('/api/v1/catalog', {
params: {
limit: 10,
page,
isFilter: 1,
...dataFilters
} }
}) })
@ -65,36 +24,34 @@ class LocalAPI {
} }
async getFilters() { async getFilters() {
const {data} = await this.instance.get("/api/v1/catalog/filters") const {data} = await this.instance.get<Wrapper<Filter[]>>("/api/v1/catalog/filters")
return data return data.data
} }
async getCatalogItemByCode(code: string) { async getCatalogItemByCode(code: string) {
const {data} = await this.instance.get<ResponseData>(`/api/v1/catalog/${code}`) const {data} = await this.instance.get<Wrapper<Product>>(`/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)) {
if (!fuserId) { const data = await this.createFUser()
fuserId = (await this.createFUser()).fuserId fuserId = data.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<createFUserType>('/api/v1/cart') const {data} = await this.instance.post<Wrapper<{fuser: Fuser, fuserId:number}>>('/api/v1/cart')
console.log("Fuser id is", data) console.log("Fuser id is", data)
return data return data.data!!
} }
async getCartItems() { async getCartItems() {
const {data} = await this.instance.get<getCartItemsType[]>('/api/v1/cart', { const {data} = await this.instance.get<Wrapper<CartItem[]>>('/api/v1/cart', {
params: { params: {
fuserId: await this.getFuserId()} fuserId: await this.getFuserId()}
}) })
@ -123,7 +80,11 @@ class LocalAPI {
} }
async totalProductPrice() { async totalProductPrice() {
const {data} = await this.instance.post('/api/v1/order/total', {fuserId: await this.getFuserId()}) const {data} = await this.instance.post<Wrapper<{total: number}>>('/api/v1/order/total', {fuserId: await this.getFuserId()})
return data
}
async fetchNews() {
const {data} = await this.instance.get<Wrapper<News[]>>("/api/v1/news")
return data return data
} }

View File

@ -0,0 +1,98 @@
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
}