Merge pull request 'Complete filter logic' (#2) from hotfix/hotfix-card-image into master

Reviewed-on: #2
hotfix/hotfix-swiper
Ernest Litvinenko 2024-03-21 23:55:03 +03:00
commit 50a8a51a20
3 changed files with 128 additions and 33 deletions

View File

@ -1,7 +1,7 @@
import { import {
CheckboxGroup, CheckboxGroup,
Checkbox, Checkbox,
Button, Skeleton, Accordion, AccordionItem Button, Skeleton, Accordion, AccordionItem, Pagination, Spinner
} from "@nextui-org/react"; } from "@nextui-org/react";
import Link from "next/link"; import Link from "next/link";
@ -16,37 +16,79 @@ 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 {element} from "prop-types";
type SelectedFiltersStruct = {
[key: string]: string[]
}
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, name: string, values: {id: number, value: string}[]}) => {
return ( return (
<Accordion> <Accordion>
<AccordionItem title={obj.name}> <AccordionItem title={obj.name}>
<CheckboxGroup> <CheckboxGroup>
{obj.values && obj.values.map(val => ( {obj.values && obj.values.map(val => (
<Checkbox value={String(val.id)} key={"VALUE_" + val.id}>{val.value}</Checkbox> <Checkbox onChange={
))} () => {
obj.dispatcher((prevState) => {
if (prevState[obj.code] && prevState[obj.code].includes(val.value)) {
return {
...prevState,
[obj.code]: prevState[obj.code].filter(v => v !== val.value)
}
} else {
return {
...prevState,
[obj.code]: [...(prevState[obj.code] || []), val.value]
}
}
})
}
} value={String(val.id)} key={"VALUE_" + val.id}>{val.value}</Checkbox>
))}
</CheckboxGroup> </CheckboxGroup>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
) )
} }
const FilterGenerator = ({filterPropertiesData}: Pick<InferGetStaticPropsType<typeof getStaticProps>, "filterPropertiesData">) => { const FilterGenerator = ({
filterPropertiesData,
setSelectedFilter
}: Pick<InferGetStaticPropsType<typeof getStaticProps>, "filterPropertiesData"> & {
setSelectedFilter: SelectedFiltersDispatcher
}) => {
return ( return (
<Accordion className={'filters mb-10 mr-4 lg:w-1/4 w-full'} defaultExpandedKeys={['1']}> <Accordion className={'filters mb-10 mr-4 lg:w-1/4 w-full'} defaultExpandedKeys={['1']}>
<AccordionItem title={"Фильтры"} key={"1"} aria-label={"Фильтры"} classNames={{trigger:"bg-primary px-4", base: "border-1 rounded", title: "font-bold"}}> <AccordionItem title={"Фильтры"} key={"1"} aria-label={"Фильтры"}
{filterPropertiesData.map(obj => ( classNames={{trigger: "bg-primary px-4", base: "border-1 rounded", title: "font-bold"}}>
<CheckboxUI key={obj.id} {...obj} /> {filterPropertiesData.map(obj => (
))} <CheckboxUI key={obj.id} {...obj} dispatcher={setSelectedFilter}/>
))}
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
) )
} }
type CatalogItemStruct = {
type CardProps = InferGetStaticPropsType<typeof getStaticProps>['catalog'][0] & { isFavourite?: boolean } 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 CatalogCard = (product: CardProps) => {
@ -84,7 +126,7 @@ const CatalogCard = (product: CardProps) => {
isClient && product.properties.main_image && isClient && product.properties.main_image &&
<Img src={`https://relynolli.ru/upload/${product.properties.main_image[0]}`} alt={product.name} <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"} className={"col-auto mx-auto mb-4 sm:col-span-2 sm:row-span-2"}
loader={<Skeleton className={"absolute top-0 left-0 right-3/4 rounded-[20px] h-full"}/>}/> loader={<Spinner/>}/>
} }
<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 <span
@ -122,15 +164,48 @@ const CatalogCard = (product: CardProps) => {
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 [page, setPage] = useState(1);
const [pageCount, setPageCount] = useState(1)
const [showPagination, setShowPagination] = useState(true);
useEffect(() => {
const service = new LocalAPI()
service.getCatalogItemsCount().then(data => setPageCount(Math.ceil(data / 10)))
})
const catalogQuery = useQuery<CatalogItemStruct[]>({
queryKey: ["catalog", {selectedFilters, page}], queryFn: async () => {
const service = new LocalAPI()
return await service.getCatalogItems(selectedFilters, page)
}
})
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 justify-end mb-4">
<FilterGenerator filterPropertiesData={props.filterPropertiesData}/> { !Object.values(selectedFilters).filter(elem => elem.length).length &&
<Pagination total={pageCount} initialPage={1} onChange={(page) => {
setPage(page)
}}/>
}
</div>
<div className="flex flex-col justify-between lg:flex-row">
<FilterGenerator filterPropertiesData={props.filterPropertiesData}
setSelectedFilter={setSelectedFilters}/>
<div className="products grid grid-cols-1 2xl:grid-cols-2 gap-7 lg:w-3/4 h-fit w-full"> <div className="products grid grid-cols-1 2xl:grid-cols-2 gap-7 lg:w-3/4 h-fit w-full">
{ {catalogQuery.isFetching &&
props.catalog.map(product => Array(10).fill(null).map((_, index) => <Skeleton key={index}
className={"h-[250px] rounded-[20px]"}/>)
}
{catalogQuery.data &&
catalogQuery.data.map(product =>
<> <>
<CatalogCard key={product.id} {...product} <CatalogCard key={product.id} {...product}
isFavourite={favourites.includes(product.id)}/> isFavourite={favourites.includes(product.id)}/>
@ -150,25 +225,13 @@ export const getStaticProps = async () => {
const filterData = await service.getFilters() as { const filterData = await service.getFilters() as {
id: number, id: number,
name: string, name: string,
values: { id: number, value: string }[]
}[]
const catalogData = await service.getCatalogItems() as {
id: number
code: string, code: string,
name: string, values: { id: number, value: string }[]
properties: {
[key: string]: string | string[]
}
detailText: string,
price: {
[key: string]: number | null
}
}[] }[]
return { return {
props: { props: {
filterPropertiesData: filterData, filterPropertiesData: filterData,
catalog: catalogData
} }
} }
} }

View File

@ -12,7 +12,7 @@ const Hero = () => {
<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 className="w-11/12 mx-auto lg:m-0 xl:w-1/2 flex flex-col bg-black bg-opacity-25 py-4 xl:px-7 px-2 rounded-[20px]">
<h2 className={"text-2xl leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-[52px]"}>Оформите заказ <h2 className={"text-2xl leading-[100%] xl:text-title-4 2xl:text-title-2 mb-4 xl:mb-[52px]"}>Оформите заказ
на сумму от 5000</h2> на сумму от 5000</h2>
<span className={"text-sm xl:text-subtitle-1 leading-inherit text-subtitle-1 mb-4 xl:mb-24"}>До 15 января и получите бесплатную доставку</span> <span className={"text-sm xl:text-subtitle-1 leading-inherit text-subtitle-1 mb-4 xl:mb-24"}>И получите бесплатную доставку</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"} 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>

View File

@ -27,8 +27,40 @@ class LocalAPI {
}) })
} }
async getCatalogItems() { async getCatalogItemsCount(){
const {data} = await this.instance.get('/api/v1/catalog') 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) {
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: {
limit: 10,
page,
isFilter: 0,
}
})
return data
}
const {data} = await this.instance.get('/api/v1/catalog', {
params: {
limit: 10,
page,
isFilter: 1,
...dataFilters
}
})
return data return data
} }