parent
adceb78cf7
commit
670ae72b1a
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,10 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import DropdownCategories from './dropdown-categories';
|
||||
|
||||
describe('DropdownCategories', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<DropdownCategories />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,42 @@
|
||||
import styles from './dropdown-categories.module.scss';
|
||||
import {PropsWithChildren, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
export interface DropdownCategoriesProps extends PropsWithChildren {
|
||||
title?: string;
|
||||
collapse?: boolean;
|
||||
|
||||
count?: number;
|
||||
}
|
||||
|
||||
|
||||
export function DropdownCategories({children, title = '', count = null, collapse = false}: DropdownCategoriesProps) {
|
||||
const [visible, setVisible] = useState(!collapse);
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<header className="flex justify-between grow px-6 py-2 align-middle items-center cursor-pointer"
|
||||
onClick={() => setVisible(!visible)}>
|
||||
<h2 className="overflow-y-auto text-md text-gray-400 flex items-center">
|
||||
{title}
|
||||
{count && (
|
||||
<span className="inline-flex items-center justify-center w-6 h-6 ml-2 text-xs font-semibold text-blue-800 bg-blue-200 rounded-full">
|
||||
{count}
|
||||
</span>)}
|
||||
</h2>
|
||||
{visible && (<FontAwesomeIcon icon={faChevronDown}
|
||||
size={'sm'}
|
||||
className="text-gray-400" />)}
|
||||
{!visible && (<FontAwesomeIcon icon={faChevronUp}
|
||||
size={'sm'}
|
||||
className="text-gray-400" />)}
|
||||
</header>
|
||||
<main className={visible ? 'block' : 'hidden'}>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DropdownCategories;
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,10 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import DropdownSection from './dropdown-section';
|
||||
|
||||
describe('DropdownComponent', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<DropdownSection />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,30 @@
|
||||
import {PropsWithChildren, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import styles from './dropdown-section.module.scss';
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
export interface DropdownComponentProps extends PropsWithChildren {
|
||||
title?: string;
|
||||
collapse?: boolean;
|
||||
}
|
||||
|
||||
export function DropdownSection({children, title = '', collapse = false}: DropdownComponentProps) {
|
||||
const [visible, setVisible] = useState(!collapse);
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<header className="flex justify-between grow px-6 py-4 align-middle items-center cursor-pointer"
|
||||
onClick={() => setVisible(!visible)}>
|
||||
<h1 className="overflow-y-auto text-lg text-gray-600">{title}</h1>
|
||||
{visible && (<FontAwesomeIcon icon={faChevronDown} className="text-gray-600"/>)}
|
||||
{!visible && (<FontAwesomeIcon icon={faChevronUp} className="text-gray-600"/>)}
|
||||
</header>
|
||||
<main className={visible ? 'block' : 'hidden'}>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DropdownSection;
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,5 @@
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const DynamicComponent = ({children}) => <>{children}</>
|
||||
|
||||
export default dynamic(() => Promise.resolve(DynamicComponent), {ssr: false})
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,10 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import ShopAmountRange from './shop-amount-range';
|
||||
|
||||
describe('ShopAmountRange', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<ShopAmountRange />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,41 @@
|
||||
import InputRange from "react-input-range";
|
||||
import {useEffect, useState} from "react";
|
||||
import {PriceRange} from "../../libs/utils";
|
||||
import {isEmpty} from "radash";
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
export interface ShopAmountRangeProps {
|
||||
range?: PriceRange;
|
||||
onChange?: (range: PriceRange) => void;
|
||||
}
|
||||
|
||||
export function ShopAmountRange({range = {min: 10, max: 599}, onChange}: ShopAmountRangeProps) {
|
||||
const [value, setValue] = useState(!isEmpty(range) ? range : {min: 10, max: 599});
|
||||
|
||||
useEffect(() => {
|
||||
setValue(!isEmpty(range) ? range : {min: 10, max: 599});
|
||||
}, [range]);
|
||||
const inputRangeChange = (value) => setValue(value);
|
||||
const inputRangeChangeComplete = (value) => {
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-5 pb-4">
|
||||
<section className="my-3 p-3 h-[40px]">
|
||||
<InputRange
|
||||
onChange={inputRangeChange}
|
||||
onChangeComplete={inputRangeChangeComplete}
|
||||
value={value}
|
||||
minValue={10}
|
||||
step={15}
|
||||
formatLabel={value => `${value} €`}
|
||||
maxValue={600} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShopAmountRange;
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,10 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import ShopAside from './shop-aside';
|
||||
|
||||
describe('ShopAside', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<ShopAside />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,123 @@
|
||||
import {buildShopParamsGET, findQueryParamsValue, PriceRange} from "../../libs/utils";
|
||||
|
||||
import DropdownSection from "../dropdown-component/dropdown-section";
|
||||
import ShopCategories from "../shop-categories/shop-categories";
|
||||
import ShopAmountRange from "../shop-amount-range/shop-amount-range";
|
||||
import ShopFilters from "../shop-filters/shop-filters";
|
||||
import ShopSearch from "../shop-search/shop-search";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useRouter} from "next/router";
|
||||
import {isEqual} from "radash";
|
||||
|
||||
export function ShopAside({sections = [], queries = [], filters = []}) {
|
||||
const [filterMap, setFilterMap] = useState(null);
|
||||
const [filtersSelected, setFiltersSelected] = useState(findQueryParamsValue(queries, 'filters') || []);
|
||||
const [priceRange, setPriceRange] = useState(findQueryParamsValue(queries, 'facets') as PriceRange);
|
||||
const [hasFilters, setHasFilters] = useState(false);
|
||||
const router = useRouter();
|
||||
const validFilters = () => {
|
||||
const list = [];
|
||||
if (filterMap) {
|
||||
Array.from(filterMap).reduce((array: number[], map: [number, Map<number, boolean>]) => {
|
||||
Array.from(map[1].entries()).forEach((value: [number, boolean]) => array.push(value[0]));
|
||||
return array;
|
||||
}, list);
|
||||
}
|
||||
|
||||
router.push(`${router.pathname}${buildShopParamsGET(
|
||||
findQueryParamsValue(queries, 'sort'),
|
||||
findQueryParamsValue(queries, 'page'),
|
||||
priceRange,
|
||||
findQueryParamsValue(queries, 'cat'),
|
||||
list,
|
||||
findQueryParamsValue(queries, 'search'),
|
||||
)}`);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!findQueryParamsValue(queries, 'filters')) {
|
||||
setFilterMap(new Map());
|
||||
setFiltersSelected([]);
|
||||
} else {
|
||||
setFiltersSelected(findQueryParamsValue(queries, 'filters') || []);
|
||||
}
|
||||
if (!findQueryParamsValue(queries, 'facets')) {
|
||||
setPriceRange(null);
|
||||
}
|
||||
}, [queries]);
|
||||
|
||||
useEffect(() => {
|
||||
setHasFilters((!isEqual(filterMap, null) && filterMap.size !== 0) || !isEqual(priceRange, null));
|
||||
}, [filterMap]);
|
||||
|
||||
const updatePriceRange = (range: PriceRange) => {
|
||||
setPriceRange(range);
|
||||
}
|
||||
|
||||
const updateFilterMap = (map: Map<number, Map<number, boolean>>) => {
|
||||
setFilterMap(map);
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
router.push(`${router.pathname}${buildShopParamsGET(
|
||||
findQueryParamsValue(queries, 'sort'),
|
||||
findQueryParamsValue(queries, 'page'),
|
||||
null,
|
||||
findQueryParamsValue(queries, 'cat'),
|
||||
null,
|
||||
findQueryParamsValue(queries, 'search'),
|
||||
)}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShopSearch queries={queries} />
|
||||
<div className="rounded-lg mx-2 md:mx-0 mt-5 md:mt-0 mb-5 shadow-sm
|
||||
bg-white
|
||||
dark:bg-gray-800">
|
||||
<DropdownSection title="Catégogies">
|
||||
<ShopCategories queries={queries}
|
||||
sections={sections} />
|
||||
</DropdownSection>
|
||||
</div>
|
||||
<div className="rounded-lg mx-2 md:mx-0 mt-5 md:mt-0 mb-5 shadow-sm
|
||||
bg-white
|
||||
dark:bg-gray-800">
|
||||
|
||||
{hasFilters && (
|
||||
<div className="pt-5 px-5 flex justify-end">
|
||||
<button onClick={() => resetFilters()}
|
||||
className="px-3 py-2 text-xs font-medium text-center rounded-lg border
|
||||
text-gray-900 bg-white border-gray-200
|
||||
focus:outline-none hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200
|
||||
dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">Réinitialiser les filtres
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<DropdownSection title="Prix"
|
||||
collapse={true}>
|
||||
<ShopAmountRange range={priceRange}
|
||||
onChange={(value: PriceRange) => updatePriceRange(value)} />
|
||||
</DropdownSection>
|
||||
<hr className="border-gray-100 dark:border-gray-700 w-full" />
|
||||
<DropdownSection title="Filtres">
|
||||
<ShopFilters filters={filters}
|
||||
filtersSelected={filtersSelected}
|
||||
onChange={(map: Map<number, Map<number, boolean>>) => updateFilterMap(map)} />
|
||||
</DropdownSection>
|
||||
<hr className="border-gray-100 dark:border-gray-700 w-full mb-5" />
|
||||
<div className="px-5">
|
||||
<button onClick={() => validFilters()}
|
||||
className="font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-5 w-full
|
||||
text-white bg-blue-700
|
||||
hover:bg-blue-800 focus:ring-4 focus:ring-blue-300
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
Appliquer les filtres
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShopAside;
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,10 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import ShopCatalog from './shop-catalog';
|
||||
|
||||
describe('ShopCatalog', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<ShopCatalog />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,58 @@
|
||||
import {buildShopParamsGET, findQueryParamsValue, getShopSortList, SortType} from "../../libs/utils";
|
||||
import BlogPagination from "../blog-pagination/blog-pagination";
|
||||
import {useRouter} from "next/router";
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
export interface ShopCatalogProps {
|
||||
}
|
||||
|
||||
export function ShopCatalog({
|
||||
queries = [],
|
||||
products = [],
|
||||
paginator = {pagination: {}}
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
const sortCatalog = (event) => {
|
||||
const value = event.target.value;
|
||||
router.push(`${router.pathname}${buildShopParamsGET(
|
||||
value as SortType,
|
||||
findQueryParamsValue(queries, 'page'),
|
||||
findQueryParamsValue(queries, 'range'),
|
||||
findQueryParamsValue(queries, 'cat'),
|
||||
findQueryParamsValue(queries, 'filters'),
|
||||
findQueryParamsValue(queries, 'search')
|
||||
)}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="flex flex-col items-end w-full">
|
||||
<div className="max-w-[300px] text-right">
|
||||
<select id="countries"
|
||||
value={findQueryParamsValue(queries, 'sort')}
|
||||
onChange={sortCatalog}
|
||||
className="border text-sm rounded-lg block w-full p-2.5
|
||||
bg-white border-gray-300 text-gray-900
|
||||
focus:ring-blue-500 focus:border-blue-500
|
||||
dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
|
||||
{getShopSortList().map((val, index: number) => (
|
||||
<option key={'sort-' + index}
|
||||
value={val.value}>{val.label}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
</header>
|
||||
<hr className="border-gray-200 dark:border-gray-700 w-full mt-3 mb-5" />
|
||||
<main>
|
||||
{products && products.length === 0 && (<p>Il n'y aucun produit pour l'instant</p>)}
|
||||
{products && products.length > 0 && (<p>Produit</p>)}
|
||||
</main>
|
||||
<footer className="mt-10">
|
||||
<BlogPagination paginator={paginator} />
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShopCatalog;
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,10 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import ShopCategories from './shop-categories';
|
||||
|
||||
describe('ShopCategories', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<ShopCategories />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,57 @@
|
||||
/* eslint-disable-next-line */
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import delve from "dlv";
|
||||
import {useRouter} from "next/router";
|
||||
|
||||
import {buildShopParamsGET, findQueryParamsValue} from "../../libs/utils";
|
||||
|
||||
export interface ShopCategoriesProps {
|
||||
sections?: object[];
|
||||
onChange?: (map: Map<number, boolean>) => void,
|
||||
queries: { param: string, value: any }[];
|
||||
}
|
||||
|
||||
export function ShopCategories({sections = [], queries = [], onChange = () => null}: ShopCategoriesProps) {
|
||||
const router = useRouter();
|
||||
const [category, setCategory] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setCategory(findQueryParamsValue(queries, 'cat'));
|
||||
}, [queries]);
|
||||
const navigateTo = (slug) => router.push(`${router.pathname}${buildShopParamsGET(
|
||||
findQueryParamsValue(queries, 'sort'),
|
||||
findQueryParamsValue(queries, 'page'),
|
||||
findQueryParamsValue(queries, 'range'),
|
||||
slug,
|
||||
findQueryParamsValue(queries, 'filters'),
|
||||
findQueryParamsValue(queries, 'search')
|
||||
)}`);
|
||||
|
||||
const isActive = (slug) => slug === category;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full px-3 pb-4 overflow-y-auto">
|
||||
<ul className="space-y-2">
|
||||
{sections.map((section: object, sectionIndex: number) => (
|
||||
<Fragment key={'accordion-' + sectionIndex}>
|
||||
<li>
|
||||
<div onClick={() => navigateTo(delve(section, 'attributes.slug', null))}
|
||||
className={"flex items-center py-2 text-base font-light rounded-lg cursor-pointer " +
|
||||
(isActive(delve(section, 'attributes.slug', null)) ? "bg-gray-100 text-blue-600 hover:bg-gray-100 " : "hover:bg-gray-50 ") +
|
||||
"text-gray-900 " +
|
||||
"dark:text-white dark:hover:bg-gray-700"}>
|
||||
<span className="ml-3">
|
||||
{delve(section, 'attributes.label', 'N/A')}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</Fragment>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShopCategories;
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,10 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import ShopFilters from './shop-filters';
|
||||
|
||||
describe('ShopFilters', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<ShopFilters />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,93 @@
|
||||
import delve from "dlv";
|
||||
|
||||
import DropdownCategories from "../dropdown-categories/dropdown-categories";
|
||||
import {useEffect, useState} from "react";
|
||||
import {isEmpty} from "radash";
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
export interface ShopFiltersProps {
|
||||
filters?: object[];
|
||||
filtersSelected?: number[];
|
||||
|
||||
onChange?: (map: Map<number, Map<number, boolean>>) => void;
|
||||
}
|
||||
|
||||
export function ShopFilters({filters = [], filtersSelected = [], onChange = null}: ShopFiltersProps) {
|
||||
const [map, setMap] = useState(new Map());
|
||||
const [count, setCount] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
setMap(new Map());
|
||||
setCount({});
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
filters.forEach((filter: object) => {
|
||||
const filterItems: number[] = delve(filter, 'attributes.filter_items.data', []).map((item) => item.id);
|
||||
if (filterItems.some((item: number) => filtersSelected.includes(item))) {
|
||||
const itemIds: number[] = filterItems.filter((item: number) => filtersSelected.includes(item));
|
||||
itemIds.forEach((itemId) => populateMap(delve(filter, 'id', null), itemId.toString(), true))
|
||||
}
|
||||
});
|
||||
|
||||
onChange(map);
|
||||
}, [filtersSelected]);
|
||||
|
||||
const setMapValue = (sectionId, event) => {
|
||||
const {checked, value} = event.target || event.detail;
|
||||
populateMap(sectionId, value, checked);
|
||||
|
||||
if (onChange) {
|
||||
onChange(map);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultChecked = (sectionId, itemId) => map.has(sectionId) && map.get(sectionId).has(itemId) ? true : false;
|
||||
|
||||
const populateMap = (sectionId: string, itemId: string, checked: boolean): void => {
|
||||
if (!map.has(sectionId)) {
|
||||
map.set(sectionId, new Map());
|
||||
}
|
||||
if (checked) {
|
||||
map.get(sectionId).set(parseInt(itemId, 10), checked);
|
||||
} else {
|
||||
map.get(sectionId).delete(parseInt(itemId, 10));
|
||||
}
|
||||
setMap(map);
|
||||
count[sectionId] = map.get(sectionId).size;
|
||||
setCount({...count});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pb-4">
|
||||
{filters.map((filter: object, index: number) => (
|
||||
<DropdownCategories key={'category-' + index}
|
||||
collapse={true}
|
||||
count={!isEmpty(count[delve(filter, 'id', null)]) || count[delve(filter, 'id', null)] !== 0 ? count[delve(filter, 'id', null)] : null}
|
||||
title={delve(filter, 'attributes.label', 'N/A')}>
|
||||
{delve(filter, 'attributes.filter_items.data', []).map((item, itemIndex: number) => (
|
||||
<div key={'category-item-' + index + '-' + itemIndex}
|
||||
className="flex items-center align-middle h-[40px] px-6 rounded-l grow
|
||||
hover:bg-gray-100">
|
||||
<input id={delve(item, 'attributes.slug', 'N/A')}
|
||||
type="checkbox"
|
||||
value={delve(item, 'id', null)}
|
||||
checked={defaultChecked(delve(filter, 'id', null), delve(item, 'id', null))}
|
||||
onChange={(event) => setMapValue(delve(filter, 'id', null), event)}
|
||||
className="w-4 h-4 rounded
|
||||
text-blue-600 bg-gray-100 border-gray-300
|
||||
focus:ring-blue-500
|
||||
dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
|
||||
<label htmlFor={delve(item, 'attributes.slug', 'N/A')}
|
||||
className="ml-2 text-sm font-light text-gray-800 dark:text-gray-300 flex flex-1 cursor-pointer">
|
||||
{delve(item, 'attributes.label', 'N/A')}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</DropdownCategories>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShopFilters;
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,10 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import ShopSearch from './shop-search';
|
||||
|
||||
describe('ShopSearch', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<ShopSearch />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,103 @@
|
||||
import {useRouter} from "next/router";
|
||||
import {useState} from "react";
|
||||
|
||||
import styles from './shop-search.module.scss';
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faMagnifyingGlass} from "@fortawesome/free-solid-svg-icons";
|
||||
import {buildShopParamsGET, findQueryParamsValue} from "../../libs/utils";
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
export interface ShopSearchProps {
|
||||
queries: { param: string, value: any }[];
|
||||
}
|
||||
|
||||
export function ShopSearch({queries = []}: ShopSearchProps) {
|
||||
const router = useRouter();
|
||||
const [search, setSearch] = useState(findQueryParamsValue(queries, 'search') || '');
|
||||
|
||||
/*useEffect(() => {
|
||||
if (subject === null) {
|
||||
const sub = new BehaviorSubject('');
|
||||
const client = new MeiliSearch({
|
||||
host: environment.meiliUrl,
|
||||
apiKey: environment.meiliApiKey
|
||||
})
|
||||
setSubject(sub);
|
||||
setClient(client);
|
||||
} else {
|
||||
subject.pipe(
|
||||
map((s: string) => s.trim()),
|
||||
distinctUntilChanged(),
|
||||
filter((s: string) => s.length >= 2),
|
||||
debounceTime(200),
|
||||
map((value) => {
|
||||
setLoading(true);
|
||||
return value;
|
||||
}),
|
||||
delay(2000),
|
||||
switchMap(async (value: string) => {
|
||||
const index = await client.getIndex('product');
|
||||
const products = await index.search(value);
|
||||
setLoading(false);
|
||||
await router.push('/search');
|
||||
dispatch(setShopSearchState(products.hits));
|
||||
}),
|
||||
catchError((err) => of(false))
|
||||
).subscribe(() => setLoading(false));
|
||||
}
|
||||
}, [subject]);*/
|
||||
|
||||
const onSearchChange = (e) => setSearch(e.target.value);
|
||||
|
||||
const validSearch = (event) => {
|
||||
router.push(`${router.pathname}${buildShopParamsGET(
|
||||
findQueryParamsValue(queries, 'sort'),
|
||||
findQueryParamsValue(queries, 'page'),
|
||||
findQueryParamsValue(queries, 'range'),
|
||||
findQueryParamsValue(queries, 'cat'),
|
||||
findQueryParamsValue(queries, 'filters'),
|
||||
search,
|
||||
)}`);
|
||||
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles['container'] + " rounded-lg mx-2 md:mx-0 mt-5 md:mt-0 p-4 mb-5 shadow-sm" +
|
||||
" bg-white" +
|
||||
" dark:bg-gray-800"}>
|
||||
<label htmlFor="default-search"
|
||||
className="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Rechercher</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<svg aria-hidden="true"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="search"
|
||||
id="default-search"
|
||||
value={search}
|
||||
onChange={onSearchChange}
|
||||
className="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Rechercher un produit ..." />
|
||||
<button type="button"
|
||||
onClick={(event) => validSearch(event)}
|
||||
className="text-white absolute right-2.5 bottom-2.5 font-medium rounded-lg text-sm px-4 py-2
|
||||
bg-blue-700
|
||||
hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
<FontAwesomeIcon icon={faMagnifyingGlass} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShopSearch;
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,63 @@
|
||||
import axios from "axios";
|
||||
import delve from "dlv";
|
||||
|
||||
import {environment} from "../../../environments/environment";
|
||||
import SeoConfig from "../../../components/seo-config/seo-config";
|
||||
import Layout from "../../../components/layout/layout";
|
||||
import ShopSearch from "../../../components/shop-search/shop-search";
|
||||
import Categories from "../../../components/categories/categories";
|
||||
import BlogPagination from "../../../components/blog-pagination/blog-pagination";
|
||||
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
export interface ResultProps {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
const {query} = context;
|
||||
|
||||
let postsUrl = `${environment.strapiApiUrl}/articles?populate=deep&sort=publishedAt:DESC`;
|
||||
if (query && query.page) {
|
||||
postsUrl += `$pagination[page]=${query.page}`;
|
||||
}
|
||||
|
||||
const categories = await axios.get(`${environment.strapiApiUrl}/categories?populate=deep`);
|
||||
const posts = await axios.get(postsUrl);
|
||||
return {
|
||||
props: {
|
||||
categories: delve(categories, 'data.data', []),
|
||||
lastPublished: delve(posts, 'data.data', []),
|
||||
paginator: delve(posts, 'data.meta', {pagination: {}}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function Filter({menuHeader, menuFooter, seo, categories, lastPublished, paginator}) {
|
||||
return (
|
||||
<>
|
||||
<SeoConfig {...seo} />
|
||||
<Layout menuHeader={menuHeader}
|
||||
menuFooter={menuFooter}
|
||||
headerTransparent={true}>
|
||||
<main className={styles['blog-container'] + " w-full my-4 px-2 lg:px-0"}>
|
||||
<section className="container max-w-screen-xl mx-auto relative flex flex-col md:flex-row">
|
||||
<aside className="grow md:basis-2/6 md:mx-0 md:pr-5">
|
||||
<ShopSearch />
|
||||
<Categories items={categories} />
|
||||
</aside>
|
||||
|
||||
<section className="grow md:basis-4/6 mx-2">
|
||||
<footer className="mt-10">
|
||||
<p>Filter page!</p>
|
||||
<BlogPagination paginator={paginator} />
|
||||
</footer>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Filter;
|
||||
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@ -0,0 +1,63 @@
|
||||
import axios from 'axios';
|
||||
import delve from 'dlv';
|
||||
|
||||
import SeoConfig from "../../../components/seo-config/seo-config";
|
||||
import Layout from "../../../components/layout/layout";
|
||||
import ShopSearch from "../../../components/shop-search/shop-search";
|
||||
import Categories from "../../../components/categories/categories";
|
||||
import BlogPagination from "../../../components/blog-pagination/blog-pagination";
|
||||
import {environment} from "../../../environments/environment";
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
export interface ResultProps {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
const {query} = context;
|
||||
|
||||
let postsUrl = `${environment.strapiApiUrl}/articles?populate=deep&sort=publishedAt:DESC`;
|
||||
if (query && query.page) {
|
||||
postsUrl += `$pagination[page]=${query.page}`;
|
||||
}
|
||||
|
||||
const categories = await axios.get(`${environment.strapiApiUrl}/categories?populate=deep`);
|
||||
const posts = await axios.get(postsUrl);
|
||||
return {
|
||||
props: {
|
||||
categories: delve(categories, 'data.data', []),
|
||||
lastPublished: delve(posts, 'data.data', []),
|
||||
paginator: delve(posts, 'data.meta', {pagination: {}}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function Result({menuHeader, menuFooter, seo, categories, lastPublished, paginator}) {
|
||||
return (
|
||||
<>
|
||||
<SeoConfig {...seo} />
|
||||
<Layout menuHeader={menuHeader}
|
||||
menuFooter={menuFooter}
|
||||
headerTransparent={true}>
|
||||
<main className={styles['blog-container'] + " w-full my-4 px-2 lg:px-0"}>
|
||||
<section className="container max-w-screen-xl mx-auto relative flex flex-col md:flex-row">
|
||||
<aside className="grow md:basis-2/6 md:mx-0 md:pr-5">
|
||||
<ShopSearch />
|
||||
<Categories items={categories} />
|
||||
</aside>
|
||||
|
||||
<section className="grow md:basis-4/6 mx-2">
|
||||
<footer className="mt-10">
|
||||
<p>Result page !</p>
|
||||
<BlogPagination paginator={paginator} />
|
||||
</footer>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Result;
|
||||
@ -0,0 +1,47 @@
|
||||
import {createSlice} from "@reduxjs/toolkit";
|
||||
import {HYDRATE} from "next-redux-wrapper";
|
||||
|
||||
import {AppState} from "./store";
|
||||
|
||||
export interface ShopSearchState {
|
||||
results: object[];
|
||||
}
|
||||
|
||||
// Initial state
|
||||
const initialState: ShopSearchState = {
|
||||
results: [],
|
||||
};
|
||||
|
||||
// Actual Slice
|
||||
// @ts-ignore
|
||||
export const shopSearchSlice = createSlice({
|
||||
name: "shopSearch",
|
||||
initialState,
|
||||
reducers: {
|
||||
|
||||
// Action to set the authentication status
|
||||
setShopSearchState(state, action) {
|
||||
// @ts-ignore
|
||||
state.results = action.payload;
|
||||
},
|
||||
|
||||
// Special reducer for hydrating the state. Special case for next-redux-wrapper
|
||||
extraReducers: {
|
||||
// @ts-ignore
|
||||
[HYDRATE]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
// @ts-ignore
|
||||
...action.payload.shopSearch,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
export const {setShopSearchState} = shopSearchSlice.actions;
|
||||
|
||||
export const selectShopSearchState = (state: AppState) => state.blogSearch;
|
||||
|
||||
export default shopSearchSlice.reducer;
|
||||
Loading…
Reference in new issue