hotfix/hotfix-card-image #1

Merged
ernestlitvinenko merged 2 commits from hotfix/hotfix-card-image into master 2024-03-18 03:21:57 +03:00
6 changed files with 105 additions and 83 deletions

View File

@ -22,6 +22,7 @@
"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-imask": "^7.5.0", "react-imask": "^7.5.0",
"valtio": "^1.13.0", "valtio": "^1.13.0",
"zod": "^3.22.4" "zod": "^3.22.4"

View File

@ -37,7 +37,7 @@ const Wrapper = (props: WrapperProps) => {
} }
</Breadcrumbs> </Breadcrumbs>
} }
<h1 className={"mt-4 text-title-1 font-bold italic uppercase pb-16"}>{props.title}</h1> <h1 className={"mt-4 lg:text-7xl text-3xl font-bold italic uppercase pb-16"}>{props.title}</h1>
{props.children} {props.children}

View File

@ -1,4 +1,4 @@
import {BreadcrumbItem, Breadcrumbs, Button} 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 axios from "axios";
@ -11,6 +11,7 @@ import {useSnapshot} from "valtio";
import useClient from "@/hooks/useClient"; 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'
const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => { const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => {
const {favourites} = useSnapshot(favouritesStore) const {favourites} = useSnapshot(favouritesStore)
@ -56,33 +57,32 @@ const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => {
<BreadcrumbItem>{product.name}</BreadcrumbItem> <BreadcrumbItem>{product.name}</BreadcrumbItem>
</Breadcrumbs> </Breadcrumbs>
</div> </div>
<div className="wrapper mt-12 grid grid-cols-2"> <div className="wrapper mt-12 grid grid-cols-1 lg:grid-cols-2 gap-4 items-center">
{/*TODO image*/} {isClient && product.properties.main_image && <Img src={"https://relynolli.ru/upload/" + product.properties.main_image[0]} alt={product.name} className={"object-center max-w-[300px] mx-auto"} loader={<Spinner />}/>}
{product.properties.main_image && <img src={product.properties.main_image[0]} alt="#"/>} <div className="right-block mt-4">
<div className="right-block">
<span <span
className={"text-xl text-[#E0E3E3] mb-7 block"}>Артикул: {product.properties.vendor_code}</span> className={"text-sm md:text-xl text-[#E0E3E3] mb-7 block"}>Артикул: {product.properties.vendor_code}</span>
<h1 className={"text-title-3 font-bold mb-7"}>{product.name}</h1> <h1 className={"text-2xl md:text-4xl font-bold mb-7"}>{product.name}</h1>
<div className="block"> <div className="block">
<span <span
className={"block mb-7 text-xl text-[#E0E3E3]"}>Категория: {product.properties.category}</span> className={"text-base md:text-xl block mb-7 text-[#E0E3E3]"}>Категория: {product.properties.category}</span>
<span <span
className={"block mb-7 text-xl text-[#E0E3E3]"}>Вязкость: {product.properties.viscosity}</span> className={"text-base md:text-xl block mb-7 text-[#E0E3E3]"}>Вязкость: {product.properties.viscosity}</span>
<span <span
className={"block mb-7 text-xl 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 text-xl py-8 mb-7"}><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="flex justify-between items-center w-3/4"> <div className="grid items-center grid-cols-2 lg:grid-cols-4 gap-5 pb-5">
<span className="font-bold text-title-4 text-white text-center"> <span className="font-bold text-3xl text-white text-center">
{product.price.BASE} {`${product.price.BASE}`.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')}
<span className={"block text-base text-[#E0E3E3] font-normal"}>1 шт</span> <span className={"block text-base text-[#E0E3E3] font-normal"}>1 шт</span>
</span> </span>
{isClient && {isClient &&
<Button onClick={() => toggleCart.mutate({productId: product.id, quantity: 1})} color={"primary"} className={"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.map(({id})=> id).includes(product.id) ? "В корзине" : "Добавить в корзину"}
@ -90,7 +90,7 @@ const OilCard = ({product}: InferGetStaticPropsType<typeof getStaticProps>) => {
</Button>} </Button>}
{ {
isClient && isClient &&
<Button onClick={() => toggleFavourite(product.id)} className={`px-5 rounded-[8px] h-[70px] ${favourites.includes(product.id) ? "bg-green-2 fill-black": "bg-[#E2E2E5] bg-opacity-30 fill-white"}`}> <Button onClick={() => toggleFavourite(product.id)} className={`max-w-fit justify-self-end lg:justify-self-start order-2 lg:order-none px-5 rounded-[8px] h-[70px] ${favourites.includes(product.id) ? "bg-green-2 fill-black": "bg-[#E2E2E5] bg-opacity-30 fill-white"}`}>
<Favourites /> <Favourites />
</Button> </Button>
} }

View File

@ -1,15 +1,12 @@
import { import {
Breadcrumbs,
BreadcrumbItem,
CheckboxGroup, CheckboxGroup,
Checkbox, Checkbox,
Button, Skeleton Button, Skeleton, Accordion, AccordionItem
} from "@nextui-org/react"; } from "@nextui-org/react";
import Link from "next/link"; import Link from "next/link";
import axios from "axios";
import {InferGetStaticPropsType} from "next"; import {InferGetStaticPropsType} from "next";
import FavouriteIcon from "@/../public/favourites_icon.svg"; import FavouriteIcon from "@/../public/favourites_icon.svg";
import {Dispatch, SetStateAction, useEffect, useState} from "react";
import {toggleFavourite} from "@/store/favourites"; import {toggleFavourite} from "@/store/favourites";
import {useSnapshot} from "valtio"; import {useSnapshot} from "valtio";
@ -18,89 +15,103 @@ 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";
const CheckboxUI = (obj: {id: number, name: string, values: {id: number, value: string}[]}) => {
return (
<Accordion>
<AccordionItem title={obj.name}>
<CheckboxGroup>
{obj.values && obj.values.map(val => (
<Checkbox value={String(val.id)} key={"VALUE_" + val.id}>{val.value}</Checkbox>
))}
</CheckboxGroup>
</AccordionItem>
</Accordion>
)
}
const FilterGenerator = ({filterPropertiesData}: Pick<InferGetStaticPropsType<typeof getStaticProps>, "filterPropertiesData">) => { const FilterGenerator = ({filterPropertiesData}: Pick<InferGetStaticPropsType<typeof getStaticProps>, "filterPropertiesData">) => {
return ( return (
<form className={'filters mb-10'}> <Accordion className={'filters mb-10 mr-4 lg:w-1/4 w-full'} defaultExpandedKeys={['1']}>
<h2 className={"text-lg mb-4"}>Фильтры</h2> <AccordionItem title={"Фильтры"} key={"1"} aria-label={"Фильтры"} classNames={{trigger:"bg-primary px-4", base: "border-1 rounded", title: "font-bold"}}>
{filterPropertiesData.map(obj => (
{filterPropertiesData.map( obj => ( <CheckboxUI key={obj.id} {...obj} />
<CheckboxGroup label={obj.name} key={"FILTER_"+obj.id}>
{obj.values && obj.values.map(val => (
<Checkbox value={String(val.id)} key={"VALUE_"+val.id}>{val.value}</Checkbox>
))} ))}
</CheckboxGroup> </AccordionItem>
</Accordion>
))}
</form>
) )
} }
type CardProps = InferGetStaticPropsType<typeof getStaticProps>['catalog'][0] & {isFavourite?: boolean} type CardProps = InferGetStaticPropsType<typeof getStaticProps>['catalog'][0] & { isFavourite?: boolean }
const CatalogCard = (product: CardProps) => { const CatalogCard = (product: CardProps) => {
const isClient = useClient() const isClient = useClient()
const cartItems = useQuery({queryKey: ['cart'], queryFn: async () => { const cartItems = useQuery({
const service = new LocalAPI() queryKey: ['cart'], queryFn: async () => {
return await service.getCartItems() const service = new LocalAPI()
}}) return await service.getCartItems()
const [imageIsLoading, setImageIsLoading] = useState(true);
const qs = useQueryClient()
const toggleCart = useMutation({
mutationFn: async ({productId, quantity}: { productId: number, quantity: number }) => {
const service = new LocalAPI()
if (cartItems.data!.find(({id}) => id === productId)) {
return await service.deleteCartItem(productId)
}
return await service.addCartItem(productId, quantity)
},
onSuccess: () => {
qs.invalidateQueries({queryKey: ["cart"]})
} }
}) })
const onImageLoad = () => {
setImageIsLoading(false); const qs = useQueryClient()
}
const toggleCart = useMutation({
mutationFn: async ({productId, quantity}: { productId: number, quantity: number }) => {
const service = new LocalAPI()
if (cartItems.data!.find(({id}) => id === productId)) {
return await service.deleteCartItem(productId)
}
return await service.addCartItem(productId, quantity)
},
onSuccess: () => {
qs.invalidateQueries({queryKey: ["cart"]})
}
})
return ( return (
<div className={"bg-gray-card w-full h-fit min-h-[250px] py-4 px-7 rounded-[20px] hover:shadow-md transition-shadow hover:cursor-pointer"} key={product.id}> <div
className={"bg-gray-card w-full h-fit min-h-[250px] py-4 px-7 rounded-[20px] hover:shadow-md transition-shadow hover:cursor-pointer"}
key={product.id}>
<div className={"grid grid-cols-1 sm:grid-cols-10 w-fit relative items-center"}> <div className={"grid grid-cols-1 sm:grid-cols-10 w-fit relative items-center"}>
{ {
imageIsLoading && <Skeleton className={"absolute top-0 left-0 right-3/4 rounded-[20px] h-full"} /> 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"}
product.properties.main_image && <img src={`/upload/${product.properties.main_image[0]}`} alt={product.name} className={"col-auto mx-auto mb-4 sm:col-span-2 sm:row-span-2"} width={300} height={430} onLoad={onImageLoad}/> loader={<Skeleton className={"absolute top-0 left-0 right-3/4 rounded-[20px] h-full"}/>}/>
} }
<div className="col-auto sm:col-start-4 sm:col-span-6 h-full flex flex-col justify-center"> <div className="col-auto sm:col-start-4 sm:col-span-6 h-full flex flex-col justify-center">
<span className={"text-[#52525C] font-normal text-subtitle-4 mb-2 block"}>Стандарт API: {product.properties.api_standart} Тип: {product.properties.oil_type}</span> <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> <h3 className={"font-bold text-lg uppercase text-black-3"}>{product.name}</h3>
</div> </div>
{ {
isClient ? isClient ?
<FavouriteIcon onClick={() => toggleFavourite(product.id)} className={`transition-colors absolute z-20 top-0 right-0 ${product.isFavourite ? "fill-primary" : "fill-[#E0E3E3]"}`}/> : null <FavouriteIcon onClick={() => toggleFavourite(product.id)}
className={`transition-colors absolute z-20 top-0 right-0 ${product.isFavourite ? "fill-primary" : "fill-[#E0E3E3]"}`}/> : null
} }
<div className="flex col-auto row-auto sm:row-start-2 sm:col-start-4 sm:col-span-7 justify-between w-full items-center pt-4"> <div
className="flex col-auto row-auto sm:row-start-2 sm:col-start-4 sm:col-span-7 justify-between w-full items-center pt-4">
<span className="font-bold text-xl text-black-3"> <span className="font-bold text-xl text-black-3">
{product.price.BASE} {`${product.price.BASE}`.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')}
</span> </span>
{ {
isClient ? isClient ?
<Button onClick={() => toggleCart.mutate({ <Button 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={"font-bold text-lg bg-green-2 uppercase italic text-black-3 relative z-20"}>{cartItems.data?.map(({id}) => id).includes(product.id) ? "В корзине" : "В корзину"}</Button> : null
} }
<Link href={'/catalog/' + product.code} className={'absolute top-0 left-0 z-10 w-full h-full'}/> <Link href={'/catalog/' + product.code} className={'absolute top-0 left-0 z-10 w-full h-full'}/>
</div> </div>
</div> </div>
@ -113,20 +124,21 @@ const Catalog = (props: InferGetStaticPropsType<typeof getStaticProps>) => {
const {favourites} = useSnapshot(favouritesStore) const {favourites} = useSnapshot(favouritesStore)
return (<Wrapper title={"Каталог"} breadcrumbs={[{name: "Каталог", link: "/catalog"}]}> return (<Wrapper title={"Каталог"} breadcrumbs={[{name: "Каталог", link: "/catalog"}]}>
<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 filterPropertiesData={props.filterPropertiesData}/>
<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 grid-cols-1 2xl:grid-cols-2 gap-7 lg:w-3/4 h-fit w-full">
{ {
props.catalog.map(product => props.catalog.map(product =>
<> <>
<CatalogCard key={product.id} {...product} isFavourite={favourites.includes(product.id)} /> <CatalogCard key={product.id} {...product}
</> isFavourite={favourites.includes(product.id)}/>
) </>
} )
</div> }
</div> </div>
</div>
</Wrapper> </Wrapper>
) )
} }
@ -135,7 +147,11 @@ 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 {id: number, name: string, values: {id: number, value: string}[]}[] const filterData = await service.getFilters() as {
id: number,
name: string,
values: { id: number, value: string }[]
}[]
const catalogData = await service.getCatalogItems() as { const catalogData = await service.getCatalogItems() as {
id: number id: number
code: string, code: string,

View File

@ -19,7 +19,7 @@
} }
.wrapper { .wrapper {
@apply 2xl:max-w-[1440px] max-w-full sm:max-w-[540px] md:max-w-[720px] lg:max-w-[960px] xl:max-w-[1140px] mx-auto; @apply 2xl:max-w-[1440px] max-w-[90%] sm:max-w-[540px] md:max-w-[720px] lg:max-w-[960px] xl:max-w-[1140px] mx-auto;
} }
} }

View File

@ -5497,6 +5497,11 @@ react-hook-form@^7.50.1:
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.50.1.tgz#f6aeb17a863327e5a0252de8b35b4fc8990377ed" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.50.1.tgz#f6aeb17a863327e5a0252de8b35b4fc8990377ed"
integrity sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ== integrity sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ==
react-image@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/react-image/-/react-image-4.1.0.tgz#92f2d4a809a178b3bf69acd7bad7da7aa5e7364c"
integrity sha512-qwPNlelQe9Zy14K2pGWSwoL+vHsAwmJKS6gkotekDgRpcnRuzXNap00GfibD3eEPYu3WCPlyIUUNzcyHOrLHjw==
react-imask@^7.5.0: react-imask@^7.5.0:
version "7.5.0" version "7.5.0"
resolved "https://registry.yarnpkg.com/react-imask/-/react-imask-7.5.0.tgz#ba751c3b29c263fc47293e49755a5f0a369a12f8" resolved "https://registry.yarnpkg.com/react-imask/-/react-imask-7.5.0.tgz#ba751c3b29c263fc47293e49755a5f0a369a12f8"