diff options
Diffstat (limited to 'src/components/elements')
| -rw-r--r-- | src/components/elements/Alert.js | 19 | ||||
| -rw-r--r-- | src/components/elements/BottomPopup.js | 25 | ||||
| -rw-r--r-- | src/components/elements/ConfirmAlert.js | 25 | ||||
| -rw-r--r-- | src/components/elements/Fields.js | 21 | ||||
| -rw-r--r-- | src/components/elements/Filter.js | 176 | ||||
| -rw-r--r-- | src/components/elements/Image.js | 13 | ||||
| -rw-r--r-- | src/components/elements/LineDivider.js | 7 | ||||
| -rw-r--r-- | src/components/elements/Link.js | 9 | ||||
| -rw-r--r-- | src/components/elements/Pagination.js | 58 | ||||
| -rw-r--r-- | src/components/elements/ProgressBar.js | 25 | ||||
| -rw-r--r-- | src/components/elements/Spinner.js | 13 |
11 files changed, 391 insertions, 0 deletions
diff --git a/src/components/elements/Alert.js b/src/components/elements/Alert.js new file mode 100644 index 00000000..914d1590 --- /dev/null +++ b/src/components/elements/Alert.js @@ -0,0 +1,19 @@ +const Alert = ({ children, className, type }) => { + let typeClass = ''; + switch (type) { + case 'info': + typeClass = ' bg-blue-100 text-blue-900 border-blue-400 ' + break; + case 'success': + typeClass = ' bg-green-100 text-green-900 border-green-400 ' + break; + case 'warning': + typeClass = ' bg-yellow-100 text-yellow-900 border-yellow-400 ' + break; + } + return ( + <div className={"rounded-md w-full text-medium p-3 border" + typeClass + className}>{children}</div> + ); +} + +export default Alert;
\ No newline at end of file diff --git a/src/components/elements/BottomPopup.js b/src/components/elements/BottomPopup.js new file mode 100644 index 00000000..deb1b895 --- /dev/null +++ b/src/components/elements/BottomPopup.js @@ -0,0 +1,25 @@ +import CloseIcon from "@/icons/close.svg"; + +const BottomPopup = ({ + active = false, + title, + children, + closePopup = () => {} +}) => { + return ( + <> + <div className={"menu-overlay " + (active ? 'block' : 'hidden')} onClick={closePopup} /> + <div className={`fixed w-full z-[60] py-6 px-4 bg-white rounded-t-3xl idt-transition ${active ? 'bottom-0' : 'bottom-[-100%]'}`}> + <div className="flex justify-between items-center mb-5"> + <h2 className="text-xl font-semibold">{ title }</h2> + <button onClick={closePopup}> + <CloseIcon className="w-7" /> + </button> + </div> + { children } + </div> + </> + ); +}; + +export default BottomPopup;
\ No newline at end of file diff --git a/src/components/elements/ConfirmAlert.js b/src/components/elements/ConfirmAlert.js new file mode 100644 index 00000000..27155011 --- /dev/null +++ b/src/components/elements/ConfirmAlert.js @@ -0,0 +1,25 @@ +const ConfirmAlert = ({ + title, + caption, + show, + onClose, + onSubmit, +}) => { + return ( + <> + {show && ( + <div className="menu-overlay" onClick={onClose}></div> + )} + <div className={"p-4 rounded border bg-white border-gray_r-6 fixed top-[50%] left-[50%] translate-x-[-50%] z-[70] w-[80%] translate-y-[-50%] " + (show ? "block" : "hidden")}> + <p className="h2 mb-2">{title}</p> + <p className="text-gray_r-11 mb-6">{caption}</p> + <div className="flex gap-x-2"> + <button className="flex-1 btn-light" onClick={onClose}>Batal</button> + <button className="flex-1 btn-red" onClick={onSubmit}>Hapus</button> + </div> + </div> + </> + ); +}; + +export default ConfirmAlert;
\ No newline at end of file diff --git a/src/components/elements/Fields.js b/src/components/elements/Fields.js new file mode 100644 index 00000000..586a6a22 --- /dev/null +++ b/src/components/elements/Fields.js @@ -0,0 +1,21 @@ +import ReactSelect from "react-select"; + +const Select = ({ + field, + ...props +}) => ( + <> + <ReactSelect + classNamePrefix="form-select" + ref={field.ref} + onChange={(option) => field.onChange(option.value)} + value={field.value ? props.options.find(option => option.value === field.value) : ''} + isDisabled={props.disabled} + {...props} + /> + </> +); + +export { + Select +};
\ No newline at end of file diff --git a/src/components/elements/Filter.js b/src/components/elements/Filter.js new file mode 100644 index 00000000..f2051ba8 --- /dev/null +++ b/src/components/elements/Filter.js @@ -0,0 +1,176 @@ +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import BottomPopup from "./BottomPopup"; + +const Filter = ({ + isActive, + closeFilter, + defaultRoute, + defaultPriceFrom, + defaultPriceTo, + defaultCategory, + defaultBrand, + defaultOrderBy, + searchResults, + disableFilter = [] +}) => { + const router = useRouter(); + + const [priceFrom, setPriceFrom] = useState(defaultPriceFrom); + const [priceTo, setPriceTo] = useState(defaultPriceTo); + const [orderBy, setOrderBy] = useState(defaultOrderBy); + const [selectedCategory, setSelectedCategory] = useState(defaultCategory); + const [selectedBrand, setSelectedBrand] = useState(defaultBrand); + const [categories, setCategories] = useState([]); + const [brands, setBrands] = useState([]); + + const filterRoute = () => { + let filterRoute = []; + let filterRoutePrefix = '?'; + if (selectedBrand) filterRoute.push(`brand=${selectedBrand}`); + if (selectedCategory) filterRoute.push(`category=${selectedCategory}`); + if (priceFrom) filterRoute.push(`price_from=${priceFrom}`); + if (priceTo) filterRoute.push(`price_to=${priceTo}`); + if (orderBy) filterRoute.push(`order_by=${orderBy}`); + + if (defaultRoute.includes('?')) filterRoutePrefix = '&'; + if (filterRoute.length > 0) { + filterRoute = filterRoutePrefix + filterRoute.join('&'); + } else { + filterRoute = ''; + } + + return defaultRoute + filterRoute; + } + + useEffect(() => { + const filterCategory = searchResults.facet_counts.facet_fields.category_name_str.filter((category, index) => { + if (index % 2 == 0) { + const productCountInCategory = searchResults.facet_counts.facet_fields.category_name_str[index + 1]; + if (productCountInCategory > 0) return category; + } + }); + setCategories(filterCategory); + + const filterBrand = searchResults.facet_counts.facet_fields.brand_str.filter((brand, index) => { + if (index % 2 == 0) { + const productCountInBrand = searchResults.facet_counts.facet_fields.brand_str[index + 1]; + if (productCountInBrand > 0) return brand; + } + }); + setBrands(filterBrand); + }, [searchResults]); + + const submit = (e) => { + e.preventDefault(); + closeFilter(); + router.push(filterRoute(), undefined, { scroll: false }); + } + + const reset = () => { + setSelectedBrand(''); + setSelectedCategory(''); + setPriceFrom(''); + setPriceTo(''); + setOrderBy(''); + } + + const changeOrderBy = (value) => { + if (orderBy == value) { + setOrderBy(''); + } else { + setOrderBy(value); + } + } + + const sortOptions = [ + { + name: 'Harga Terendah', + value: 'price-asc', + }, + { + name: 'Harga Tertinggi', + value: 'price-desc', + }, + { + name: 'Populer', + value: 'popular', + }, + { + name: 'Ready Stock', + value: 'stock', + }, + ]; + + return ( + <> + <BottomPopup active={isActive} closePopup={closeFilter} title="Filter Produk"> + <form className="flex flex-col gap-y-4" onSubmit={submit}> + {(selectedBrand || selectedCategory || priceFrom || priceTo || orderBy) && ( + <button type="button" className="text-yellow_r-11 font-semibold ml-auto" onClick={reset}> + Reset Filter + </button> + )} + + {!disableFilter.includes('orderBy') && ( + <div> + <label>Urutkan</label> + <div className="flex gap-2 mt-2 overflow-x-auto w-full"> + {sortOptions.map((sortOption, index) => ( + <button + key={index} + type="button" + className={"p-2 rounded border border-gray_r-6 flex-shrink-0" + (orderBy == sortOption.value ? ' border-yellow_r-10 bg-yellow_r-3 text-yellow_r-11' : '')} + onClick={() => changeOrderBy(sortOption.value)} + > + {sortOption.name} + </button> + ))} + </div> + </div> + )} + + {!disableFilter.includes('category') && ( + <div> + <label>Kategori</label> + <select className="form-input mt-2" value={selectedCategory} onChange={(e) => setSelectedCategory(e.target.value)}> + <option value="">Pilih kategori...</option> + {categories?.map((category, index) => ( + <option key={index} value={category}>{category}</option> + ))} + </select> + </div> + )} + + {!disableFilter.includes('brand') && ( + <div> + <label>Brand</label> + <select className="form-input mt-2" value={selectedBrand} onChange={(e) => setSelectedBrand(e.target.value)}> + <option value="">Pilih brand...</option> + {brands?.map((brand, index) => ( + <option key={index} value={brand}>{brand}</option> + ))} + </select> + </div> + )} + + {!disableFilter.includes('price') && ( + <div> + <label>Harga</label> + <div className="flex gap-x-4 mt-2 items-center"> + <input className="form-input" type="number" placeholder="Dari" value={priceFrom} onChange={(e) => setPriceFrom(e.target.value)}/> + <span>—</span> + <input className="form-input" type="number" placeholder="Sampai" value={priceTo} onChange={(e) => setPriceTo(e.target.value)}/> + </div> + </div> + )} + <button type="submit" className="btn-yellow font-semibold mt-2 w-full"> + Terapkan Filter + </button> + </form> + </BottomPopup> + </> + ) +}; + +export default Filter;
\ No newline at end of file diff --git a/src/components/elements/Image.js b/src/components/elements/Image.js new file mode 100644 index 00000000..f06272b0 --- /dev/null +++ b/src/components/elements/Image.js @@ -0,0 +1,13 @@ +import { LazyLoadImage } from "react-lazy-load-image-component"; +import 'react-lazy-load-image-component/src/effects/blur.css'; + +export default function Image({ src, alt, className = "" }) { + return ( + <LazyLoadImage + effect="blur" + src={src || '/images/noimage.jpeg'} + alt={src ? alt : 'Image Not Found - Indoteknik'} + className={className} + /> + ); +}
\ No newline at end of file diff --git a/src/components/elements/LineDivider.js b/src/components/elements/LineDivider.js new file mode 100644 index 00000000..4e8c7b52 --- /dev/null +++ b/src/components/elements/LineDivider.js @@ -0,0 +1,7 @@ +const LineDivider = () => { + return ( + <hr className="h-1 bg-gray_r-4 border-none"/> + ); +}; + +export default LineDivider;
\ No newline at end of file diff --git a/src/components/elements/Link.js b/src/components/elements/Link.js new file mode 100644 index 00000000..d354bb1b --- /dev/null +++ b/src/components/elements/Link.js @@ -0,0 +1,9 @@ +import NextLink from "next/link"; + +export default function Link({ children, ...pageProps }) { + return ( + <NextLink {...pageProps} scroll={false}> + {children} + </NextLink> + ) +}
\ No newline at end of file diff --git a/src/components/elements/Pagination.js b/src/components/elements/Pagination.js new file mode 100644 index 00000000..1cb1bca0 --- /dev/null +++ b/src/components/elements/Pagination.js @@ -0,0 +1,58 @@ +import Link from "./Link"; + +export default function Pagination({ pageCount, currentPage, url }) { + let firstPage = false; + let lastPage = false; + let dotsPrevPage = false; + let dotsNextPage = false; + let urlParameterPrefix = url.includes('?') ? '&' : '?'; + + return ( + <div className="pagination"> + {Array.from(Array(pageCount)).map((v, i) => { + let page = i + 1; + let rangePrevPage = currentPage - 2; + let rangeNextPage = currentPage + 2; + let PageComponent = <Link key={i} href={`${url + urlParameterPrefix}page=${page}`} className={"pagination-item" + (page == currentPage ? " pagination-item--active " : "")}>{page}</Link>; + let DotsComponent = <div key={i} className="pagination-dots">...</div>; + + if (pageCount == 7) { + return PageComponent; + } + + if (currentPage == 1) rangeNextPage += 3; + if (currentPage == 2) rangeNextPage += 2; + if (currentPage == 3) rangeNextPage += 1; + if (currentPage == 4) rangePrevPage -= 1; + if (currentPage == pageCount) rangePrevPage -= 3; + if (currentPage == pageCount - 1) rangePrevPage -= 2; + if (currentPage == pageCount - 2) rangePrevPage -= 1; + if (currentPage == pageCount - 3) rangeNextPage += 1; + + if (page > rangePrevPage && page < rangeNextPage) { + return PageComponent; + } + + if (page == 1 && rangePrevPage >= 1 && !firstPage) { + firstPage = true; + return PageComponent; + } + + if (page == pageCount && rangeNextPage <= pageCount && !lastPage) { + lastPage = true; + return PageComponent; + } + + if (page > currentPage && (pageCount - currentPage) > 1 && !dotsNextPage) { + dotsNextPage = true; + return DotsComponent; + } + + if (page < currentPage && (currentPage - 1) > 1 && !dotsPrevPage) { + dotsPrevPage = true; + return DotsComponent; + } + })} + </div> + ) +}
\ No newline at end of file diff --git a/src/components/elements/ProgressBar.js b/src/components/elements/ProgressBar.js new file mode 100644 index 00000000..0adedcdf --- /dev/null +++ b/src/components/elements/ProgressBar.js @@ -0,0 +1,25 @@ +import { Fragment } from "react"; + +const ProgressBar = ({ current, labels }) => { + return ( + <div className="bg-gray_r-1 flex gap-x-2 p-4 rounded-md"> + {labels.map((label, index) => ( + <Fragment key={index}> + <div className={"flex gap-x-2 items-center " + (index < current ? 'text-gray_r-12' : 'text-gray_r-11')}> + <div className={"leading-none p-2 rounded-full w-7 text-center text-caption-2 " + (index < current ? 'bg-yellow_r-9' : 'bg-gray_r-5')}> + { index + 1 } + </div> + <p className="font-medium text-caption-2">{ label }</p> + </div> + { index < (labels.length - 1) && ( + <div className="flex-1 flex items-center"> + <div className="h-0.5 w-full bg-gray_r-7"></div> + </div> + ) } + </Fragment> + ))} + </div> + ) +} + +export default ProgressBar;
\ No newline at end of file diff --git a/src/components/elements/Spinner.js b/src/components/elements/Spinner.js new file mode 100644 index 00000000..21006ecd --- /dev/null +++ b/src/components/elements/Spinner.js @@ -0,0 +1,13 @@ +const Spinner = ({ className }) => { + return ( + <div role="status"> + <svg aria-hidden="true" className={"animate-spin " + className} viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/> + <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/> + </svg> + <span className="sr-only">Loading...</span> + </div> + ) +} + +export default Spinner;
\ No newline at end of file |
