Merge branch 'feature/filter-column' into develop

feature/vendors
Näser 3 years ago
commit fd79d99444

@ -7,7 +7,7 @@ import {useDispatch} from "react-redux";
import {environment} from "../../environments/environment";
import styles from './blog-search.module.scss';
import {setSearchState} from "../../store/searchSlice";
import {setBlogSearchState} from "../../store/blogSearchSlice";
/* eslint-disable-next-line */
export interface BlogSearchProps {
@ -44,8 +44,8 @@ export function BlogSearch(props: BlogSearchProps) {
const index = await client.getIndex('article');
const articles = await index.search(value);
setLoading(false);
await router.push('/search');
dispatch(setSearchState(articles.hits));
await router.push('/shop/results');
dispatch(setBlogSearchState(articles.hits));
}),
catchError((err) => of(false))
).subscribe(() => setLoading(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 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})

@ -3,13 +3,16 @@ import {useEffect} from "react";
import delve from "dlv";
import {signOut, useSession} from "next-auth/react";
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {useRouter} from "next/router";
import {faBagShopping, faRightToBracket, faUserPlus} from "@fortawesome/free-solid-svg-icons";
import {getBackendImg, hasAvatar} from "../../libs/api";
import {faBagShopping, faRightToBracket, faUserPlus} from "@fortawesome/free-solid-svg-icons";
import {siteConfig} from "../../config";
import {buildShopParamsGET} from "../../libs/utils";
export function Header({items = []}) {
const {data: session} = useSession();
const router = useRouter();
useEffect(() => {
if (session == null) {
@ -37,6 +40,8 @@ export function Header({items = []}) {
}
});
const isActive = (item) => router.route.includes(item.attributes.url);
const renderGenerateItem = (item, index, isActive = false) => {
const value = delve(item, 'attributes', {});
return (
@ -47,8 +52,10 @@ export function Header({items = []}) {
}
const renderRegularItem = (item, isActive = false) => {
const url = item.url.includes('/shop') ? `${item.url}${buildShopParamsGET()}` : item.url;
return (
<a href={item.url}
<a href={url}
target={item.target}
title={"Accédez à la page " + item.title}
aria-label={"Accédez à la page " + item.title}
@ -249,7 +256,7 @@ export function Header({items = []}) {
id="navbar-dropdown">
<div className="container max-w-screen-xl mx-auto">
<ul className="flex flex-col md:p-4 font-medium md:flex-row md:space-x-8 md:mt-0">
{items.map((item, index: number) => renderGenerateItem(item, index))}
{items.map((item, index: number) => renderGenerateItem(item, index, isActive(item)))}
</ul>
</div>
</section>

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

@ -1,17 +1,98 @@
import delve from 'dlv';
import {ImageLoader, ImageLoaderProps} from "next/image";
import {isEmpty, isEqual} from "radash";
import {environment} from "../environments/environment";
export type ImageFormatType = "default" | "thumbnail" | "medium" | "small";
export const contentfulImageLoader: ImageLoader = ({ src, width }: ImageLoaderProps) => {
export interface PriceRange {
min: number;
max: number;
}
export type SortType = 'pas' | 'pde' | 'alp';
export interface QueryParam {
param: string;
value: PriceRange | string | number[];
}
export const buildQueriesListFromQueryParams = (query): QueryParam[] => {
const queryMap = ['sort', 'cat', 'page', 'facets', 'filters', 'search'];
return queryMap.map((param: string) => {
if (!isEmpty(query[param])) {
let value = !isEmpty(query[param]) ? query[param] : null;
if (param === 'filters') {
value = query[param].split(',').map((value: string) => parseInt(value, 10));
}
if (param === 'facets') {
const values = query[param].split(':');
value = {min: parseInt(values[0], 10), max: parseInt(values[1], 10)};
}
return ({param, value});
} else {
return null;
}
}).filter((val) => !isEmpty(val));
}
export const findQueryParamsValue = <T>(queryList: Array<{ param: string, value: T }>, params: string): T | null => {
const paramGET = queryList.find((val: { param: string, value: T }) => val.param === params);
return !isEmpty(paramGET) ? paramGET.value : null;
}
export const getShopSortList = (): Array<{ value: SortType, label: string }> => {
return [
{value: "pas", label: 'Prix croissant'},
{value: "pde", label: 'Prix décroissant'},
{value: "alp", label: 'Ordre alphabetique'},
];
}
export const buildShopParamsGET = (sort: SortType = 'pas',
page: number = null,
range: PriceRange = {
min: 0,
max: 0
},
cat: string = null,
filters: number[] = [],
search: string) => {
let url = `?sort=${sort}`;
if (!isEmpty(range) && !isEqual(range, {min: 0, max: 0})) {
url += `&facets=${range.min}:${range.max}`;
}
if (!isEmpty(filters)) {
url += `&filters=${filters.join(',')}`;
}
if (!isEmpty(cat)) {
url += `&cat=${cat}`;
}
if (!isEmpty(page)) {
url += `&page=${page}`;
}
if (!isEmpty(search)) {
url += `&search=${search}`;
}
return url;
}
export const contentfulImageLoader: ImageLoader = ({src, width}: ImageLoaderProps) => {
return `${src}?w=${width}`
}
export const getCategoryUrl = (item): string => {
const categorySlug = !delve(item, 'attributes', null) ? delve(item, 'category.data.attributes.slug', '') : delve(item, 'attributes.category.data.attributes.slug', '');
return '/blog/' + categorySlug;
}
export const getPostUrl = (item): string => {
const categorySlug = !delve(item, 'attributes', null) ? delve(item, 'category.data.attributes.slug', '') : delve(item, 'attributes.category.data.attributes.slug', '');
const postSlug = !delve(item, 'attributes', null) ? delve(item, 'slug', '') : delve(item, 'attributes.slug', '');
@ -33,6 +114,7 @@ export const getStrapiImage = (item, format: ImageFormatType = 'default') => {
return environment.strapiUrl + delve(image, "url", "/images/default.png");
}
}
export const getStrapiImageSize = (item, format: ImageFormatType = 'default'): [number, number] => {
const image = delve(item, 'attributes.image.data.attributes', {});
switch (format) {

@ -10,6 +10,9 @@ const nextConfig = {
reactStrictMode: true,
poweredByHeader: false,
output: 'standalone',
compiler: {
styledComponents: true
},
nx: {
// Set this to true if you would like to to use SVGR
// See: https://github.com/gregberge/svgr

@ -5,9 +5,12 @@ import '@fortawesome/fontawesome-svg-core/styles.css';
import {
faBagShopping,
faChalkboardUser,
faChevronDown,
faChevronUp,
faGraduationCap,
faGuitar,
faLinesLeaning,
faMagnifyingGlass,
faRightToBracket,
faScrewdriverWrench,
faTabletScreenButton,
@ -33,6 +36,9 @@ library.add(faChalkboardUser);
library.add(faLinesLeaning);
library.add(faGuitar);
library.add(faScrewdriverWrench);
library.add(faChevronDown);
library.add(faChevronUp);
library.add(faMagnifyingGlass);
interface CustomAppProps extends AppProps {
menuHeader: object[],
@ -43,10 +49,14 @@ interface CustomAppProps extends AppProps {
const getPageMetadata = async (path) => {
try {
if (path === '/_error') {
throw 'page error';
}
console.log(path);
return await axios.get(`${environment.strapiApiUrl}/pages?filters[slug]=${path === '/' ? 'home' : path.slice(1, path.length)}&populate=deep`);
} catch (e) {
console.error(e);
return Promise.resolve({})
return Promise.resolve({});
}
}
@ -88,17 +98,14 @@ CustomApp.getInitialProps = async (context) => {
const menuFooter = await axios.get(`${environment.strapiApiUrl}/menus/2?nested&populate=deep`);
let page = null;
console.log(context.ctx.query);
if (context.ctx.query.category_slug && !context.ctx.query.post_slug) {
page = await getCategoryMetadata(context.ctx.query.category_slug);
} else if (context.ctx.query.category_slug && context.ctx.query.post_slug) {
page = await getPostMetadata(context.ctx.query.post_slug);
} else if (Object.keys(context.ctx.query).length === 0) {
} else if (!context.ctx.query.category_slug && !context.ctx.query.post_slug) {
page = await getPageMetadata(path);
}
console.log(page);
return {
...appProps,
menuHeader: delve(menuHeader, 'data.data.attributes.items.data', []),

@ -9,7 +9,7 @@ import BlogSearch from "../../components/blog-search/blog-search";
import Categories from "../../components/categories/categories";
import styles from './index.module.scss';
import {useSelector} from "react-redux";
import {selectSearchState} from "../../store/searchSlice";
import {selectBlogSearchState} from "../../store/blogSearchSlice";
export async function getServerSideProps(context) {
@ -24,7 +24,7 @@ export async function getServerSideProps(context) {
}
export function Search({menuHeader, menuFooter, seo, categories, lastPublished}) {
const {results} = useSelector(selectSearchState);
const {results} = useSelector(selectBlogSearchState);
return (
<>
<SeoConfig {...seo} />

@ -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,101 @@
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 ShopAside from "../../components/shop-aside/shop-aside";
import ShopCatalog from "../../components/shop-catalog/shop-catalog";
import styles from './index.module.scss';
import {buildQueriesListFromQueryParams, SortType} from "../../libs/utils";
export async function getServerSideProps(context) {
const {query} = context;
const queryValues = buildQueriesListFromQueryParams(query);
let productsUrl = `${environment.strapiApiUrl}/products?populate=deep`;
if (query && query.page) {
productsUrl += `$pagination[page]=${query.page}`;
}
if (query && query.search) {
productsUrl += `&filters[$or][0][title][$contains]=${query.search}&filters[$or][1][content][$contains]=${query.search}&filters[$or][2][description][$contains]=${query.search}`;
}
if (query && query.sort) {
if ((query.sort as SortType) === 'pas') {
productsUrl += `&sort=[amount]:ASC`;
} else if ((query.sort as SortType) === 'pde') {
productsUrl += `&sort=[amount]:DESC`;
} else if ((query.sort as SortType) === 'alp') {
productsUrl += `&sort=[title]:ASC`;
} else {
productsUrl += `&sort=[title]:ASC`;
}
}
if (query && query.facets) {
const facets: string[] = query.facets.split(':');
productsUrl += `&filters[amount][$between]=${(parseInt(facets[0], 10) * 100)}&filters[amount][$between]=${(parseInt(facets[1], 10) * 100)}`;
}
if (query && query.filters) {
query.filters.split(',').forEach((filterId, index) => productsUrl += `&filters[filter_items][id][$in][${index}]=${filterId}`);
}
let filtersUrl = `${environment.strapiApiUrl}/filter-categories?populate=deep&sort=[filter_section][slug]:DESC`;
if (query && query.cat) {
productsUrl += `&filters[filter_items][filter_category][filter_section][slug][$eq]=${query.cat}`;
filtersUrl += `&filters[filter_section][slug][$eq]=${query.cat}`;
}
console.log(productsUrl);
const sections = await axios.get(`${environment.strapiApiUrl}/filter-sections?populate=deep`);
const products = await axios.get(productsUrl);
const filters = await axios.get(filtersUrl);
return {
props: {
sections: delve(sections, 'data.data', []),
products: delve(products, 'data.data', []),
filters: delve(filters, 'data.data', []),
paginator: delve(products, 'data.meta', {pagination: {}}),
queries: queryValues
}
}
}
export function Shop({menuHeader, menuFooter, seo, sections, products, paginator, queries = [], filters = []}) {
return (
<>
<SeoConfig {...seo} />
<Layout menuHeader={menuHeader}
menuFooter={menuFooter}
headerTransparent={true}>
<main className={styles['blog-container'] + " w-full my-4 lg:px-0"}>
<section className="container max-w-screen-xl mx-auto relative flex flex-col md:flex-row">
<aside className="grow md:basis-3/12 md:mx-0 md:pr-5">
<ShopAside queries={queries}
filters={filters}
sections={sections} />
</aside>
<section className="grow md:basis-9/12 mx-2">
<ShopCatalog products={products}
queries={queries}
paginator={paginator} />
</section>
</section>
</main>
</Layout>
</>
);
}
export default Shop;

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

@ -1,6 +1,7 @@
@import 'tailwindcss/base.css';
@import 'tailwindcss/components.css';
@import 'tailwindcss/utilities.css';
@import "react-input-range/lib/css/index.css";
html, body {
position: relative;

@ -3,24 +3,24 @@ import {HYDRATE} from "next-redux-wrapper";
import {AppState} from "./store";
export interface SearchState {
export interface BlogSearchState {
results: object[];
}
// Initial state
const initialState: SearchState = {
const initialState: BlogSearchState = {
results: [],
};
// Actual Slice
// @ts-ignore
export const searchSlice = createSlice({
name: "search",
export const blogSearchSlice = createSlice({
name: "blogSearch",
initialState,
reducers: {
// Action to set the authentication status
setSearchState(state, action) {
setBlogSearchState(state, action) {
// @ts-ignore
state.results = action.payload;
},
@ -32,7 +32,7 @@ export const searchSlice = createSlice({
return {
...state,
// @ts-ignore
...action.payload.search,
...action.payload.blogSearch,
};
},
},
@ -40,8 +40,8 @@ export const searchSlice = createSlice({
},
});
export const {setSearchState} = searchSlice.actions;
export const {setBlogSearchState} = blogSearchSlice.actions;
export const selectSearchState = (state: AppState) => state.search;
export const selectBlogSearchState = (state: AppState) => state.blogSearch;
export default searchSlice.reducer;
export default blogSearchSlice.reducer;

@ -1,11 +1,14 @@
import {Action, configureStore, ThunkAction} from "@reduxjs/toolkit";
import {searchSlice} from "./searchSlice";
import {createWrapper} from "next-redux-wrapper";
import {blogSearchSlice} from "./blogSearchSlice";
import {shopSearchSlice} from "./storeSearchSlice";
const makeStore = () =>
configureStore({
reducer: {
[searchSlice.name]: searchSlice.reducer,
[blogSearchSlice.name]: blogSearchSlice.reducer,
[shopSearchSlice.name]: shopSearchSlice.reducer,
},
devTools: true,
});

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

@ -4,6 +4,10 @@ const {join} = require('path');
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
join(
__dirname,
"../../node_modules/flowbite-react/**/*.js"
),
join(
__dirname,
'{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html}'
@ -13,6 +17,8 @@ module.exports = {
theme: {
extend: {},
},
plugins: [],
plugins: [
require('flowbite/plugin')
],
darkMode: 'class'
};

99
package-lock.json generated

@ -1416,6 +1416,37 @@
}
}
},
"@floating-ui/core": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.1.tgz",
"integrity": "sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg=="
},
"@floating-ui/dom": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.1.tgz",
"integrity": "sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==",
"requires": {
"@floating-ui/core": "^1.2.1"
}
},
"@floating-ui/react": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.16.0.tgz",
"integrity": "sha512-h+69TJSAY2R/k5rw+az56RzzDFc/Tg7EHn/qEgwkIVz56Zg9LlaRMMUvxkcvd+iN3CNFDLtEnDlsXnpshjsRsQ==",
"requires": {
"@floating-ui/react-dom": "^1.1.2",
"aria-hidden": "^1.1.3",
"tabbable": "^6.0.1"
}
},
"@floating-ui/react-dom": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.3.0.tgz",
"integrity": "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==",
"requires": {
"@floating-ui/dom": "^1.2.1"
}
},
"@fortawesome/fontawesome-common-types": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz",
@ -4492,6 +4523,14 @@
"sprintf-js": "~1.0.2"
}
},
"aria-hidden": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.2.tgz",
"integrity": "sha512-6y/ogyDTk/7YAe91T3E2PR1ALVKyM2QbTio5HwM+N1Q6CMlCKhvClyIjkckBswa0f2xJhjsfzIGa1yVSe1UMVA==",
"requires": {
"tslib": "^2.0.0"
}
},
"aria-query": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
@ -4603,6 +4642,11 @@
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"autobind-decorator": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/autobind-decorator/-/autobind-decorator-1.4.3.tgz",
"integrity": "sha512-FRzT10Vc0lzgDOhMTpm9a2kZF6Q+MMGwd6Y7OGgHigMZwGz7vpN4qH9ifiPTum8mhJQV9UqLPperHxc9yalAAA=="
},
"autoprefixer": {
"version": "10.4.13",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz",
@ -5914,6 +5958,11 @@
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==",
"dev": true
},
"debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -6153,6 +6202,11 @@
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
},
"easy-bem": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/easy-bem/-/easy-bem-1.1.1.tgz",
"integrity": "sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A=="
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@ -7440,6 +7494,17 @@
"mini-svg-data-uri": "^1.4.3"
}
},
"flowbite-react": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/flowbite-react/-/flowbite-react-0.3.8.tgz",
"integrity": "sha512-IzbpvnUBDXsdf3HflbYv2W1lmTXITizMaX4G0SYoh/GxSp+25E97yNuwdBItwtCacUU1MJLwqIYXeicAxScRfA==",
"requires": {
"@floating-ui/react": "^0.16.0",
"classnames": "^2.3.2",
"react-icons": "^4.6.0",
"react-indiana-drag-scroll": "^2.2.0"
}
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
@ -11292,6 +11357,11 @@
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
},
"radash": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/radash/-/radash-10.7.0.tgz",
"integrity": "sha512-dz5NUcGnvn080kKJnyPtqVnP4MWoiwp5qQhEQFK/qMJxCjffQd8tMR4cybxy6y5hupYa5UkSlJRSRY2F7GPBxQ=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -11369,6 +11439,30 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.42.1.tgz",
"integrity": "sha512-2UIGqwMZksd5HS55crTT1ATLTr0rAI4jS7yVuqTaoRVDhY2Qc4IyjskCmpnmdYqUNOYFy04vW253tb2JRVh+IQ=="
},
"react-icons": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz",
"integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw=="
},
"react-indiana-drag-scroll": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/react-indiana-drag-scroll/-/react-indiana-drag-scroll-2.2.0.tgz",
"integrity": "sha512-+W/3B2OQV0FrbdnsoIo4dww/xpH0MUQJz6ziQb7H+oBko3OCbXuzDFYnho6v6yhGrYDNWYPuFUewb89IONEl/A==",
"requires": {
"classnames": "^2.2.6",
"debounce": "^1.2.0",
"easy-bem": "^1.1.1"
}
},
"react-input-range": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-input-range/-/react-input-range-1.3.0.tgz",
"integrity": "sha512-G//kJqUHo7zQA5PuGZNKhuzhGcj83FJsv62tcP4Eo61DUC/0usHPYxFfIZ3zOfdMWuWEaduD6N4lNsZMmaOJgw==",
"requires": {
"autobind-decorator": "^1.3.4",
"prop-types": "^15.5.8"
}
},
"react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@ -12580,6 +12674,11 @@
}
}
},
"tabbable": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.1.1.tgz",
"integrity": "sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg=="
},
"tailwindcss": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz",

@ -23,15 +23,18 @@
"date-fns": "^2.29.3",
"dlv": "1.1.3",
"flowbite": "1.6.2",
"flowbite-react": "^0.3.8",
"meilisearch": "^0.31.1",
"next": "13.1.1",
"next-auth": "4.18.8",
"next-redux-wrapper": "^8.1.0",
"next-themes": "^0.2.1",
"radash": "^10.7.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-helmet": "6.0.0",
"react-hook-form": "7.42.1",
"react-input-range": "^1.3.0",
"react-player": "^2.11.2",
"react-redux": "^8.0.5",
"react-share": "^4.4.1",

Loading…
Cancel
Save