diff options
| author | Rafi Zadanly <zadanlyr@gmail.com> | 2023-02-17 17:07:50 +0700 |
|---|---|---|
| committer | Rafi Zadanly <zadanlyr@gmail.com> | 2023-02-17 17:07:50 +0700 |
| commit | f99e0aba70efad0deb907d8e27f09fc9f527c8a4 (patch) | |
| tree | f0ac96e4e736a1d385e32553f0e641ee27e11fd3 /src/core/components/elements | |
| parent | 90e1edab9b6a8ccc09a49fed3addbec2cbc4e4c3 (diff) | |
Refactor
Diffstat (limited to 'src/core/components/elements')
| -rw-r--r-- | src/core/components/elements/Appbar/Appbar.jsx | 33 | ||||
| -rw-r--r-- | src/core/components/elements/Badge/Badge.jsx | 33 | ||||
| -rw-r--r-- | src/core/components/elements/Divider/Divider.jsx | 11 | ||||
| -rw-r--r-- | src/core/components/elements/Image/Image.jsx | 15 | ||||
| -rw-r--r-- | src/core/components/elements/Link/Link.jsx | 17 | ||||
| -rw-r--r-- | src/core/components/elements/NavBar/NavBar.jsx | 31 | ||||
| -rw-r--r-- | src/core/components/elements/NavBar/Search.jsx | 89 | ||||
| -rw-r--r-- | src/core/components/elements/Pagination/Pagination.js | 64 | ||||
| -rw-r--r-- | src/core/components/elements/Popup/BottomPopup.jsx | 21 | ||||
| -rw-r--r-- | src/core/components/elements/Skeleton/BrandSkeleton.jsx | 8 | ||||
| -rw-r--r-- | src/core/components/elements/Skeleton/ImageSkeleton.jsx | 10 | ||||
| -rw-r--r-- | src/core/components/elements/Skeleton/ProductCardSkeleton.jsx | 15 |
12 files changed, 347 insertions, 0 deletions
diff --git a/src/core/components/elements/Appbar/Appbar.jsx b/src/core/components/elements/Appbar/Appbar.jsx new file mode 100644 index 00000000..0fe087d3 --- /dev/null +++ b/src/core/components/elements/Appbar/Appbar.jsx @@ -0,0 +1,33 @@ +import { useRouter } from "next/router" +import Link from "../Link/Link" +import { HomeIcon, Bars3Icon, ShoppingCartIcon, ChevronLeftIcon } from "@heroicons/react/24/outline" + +const AppBar = ({ title }) => { + const router = useRouter() + + return ( + <nav className="sticky top-0 z-50 bg-white shadow flex justify-between"> + <div className="flex items-center"> + <button type="button" className="p-4" onClick={() => router.back()}> + <ChevronLeftIcon className="w-6 stroke-2" /> + </button> + <div className="font-semibold text-h-md"> + { title } + </div> + </div> + <div className="flex items-center px-2"> + <Link href="/shop/cart" className="py-4 px-2"> + <ShoppingCartIcon className="w-6 text-gray_r-12" /> + </Link> + <Link href="/shop/cart" className="py-4 px-2"> + <HomeIcon className="w-6 text-gray_r-12" /> + </Link> + <Link href="/shop/cart" className="py-4 px-2"> + <Bars3Icon className="w-6 text-gray_r-12" /> + </Link> + </div> + </nav> + ) +} + +export default AppBar
\ No newline at end of file diff --git a/src/core/components/elements/Badge/Badge.jsx b/src/core/components/elements/Badge/Badge.jsx new file mode 100644 index 00000000..5d8ebd1c --- /dev/null +++ b/src/core/components/elements/Badge/Badge.jsx @@ -0,0 +1,33 @@ +const Badge = ({ + children, + type, + ...props +}) => { + return ( + <div + { ...props } + className={`${badgeStyle(type)} ${props?.className}`} + > + { children } + </div> + ) +} + +Badge.defaultProps = { + className: '' +} + +const badgeStyle = (type) => { + let className = ['rounded px-1 text-[11px]'] + switch (type) { + case 'solid-red': + className.push('bg-red_r-11 text-white') + break + case 'light': + className.push('bg-gray_r-4 text-gray_r-11') + break + } + return className.join(' ') +} + +export default Badge
\ No newline at end of file diff --git a/src/core/components/elements/Divider/Divider.jsx b/src/core/components/elements/Divider/Divider.jsx new file mode 100644 index 00000000..355cd509 --- /dev/null +++ b/src/core/components/elements/Divider/Divider.jsx @@ -0,0 +1,11 @@ +const Divider = (props) => { + return ( + <div className={`h-1 bg-gray_r-4 ${props.className}`} /> + ) +} + +Divider.defaultProps = { + className: '' +} + +export default Divider
\ No newline at end of file diff --git a/src/core/components/elements/Image/Image.jsx b/src/core/components/elements/Image/Image.jsx new file mode 100644 index 00000000..be2866e7 --- /dev/null +++ b/src/core/components/elements/Image/Image.jsx @@ -0,0 +1,15 @@ +import { LazyLoadImage } from "react-lazy-load-image-component" +import "react-lazy-load-image-component/src/effects/opacity.css" + +const Image = ({ ...props }) => ( + <LazyLoadImage + { ...props } + effect="opacity" + src={props.src || '/images/noimage.jpeg'} + alt={props.src ? props.alt : 'Image Not Found - Indoteknik'} + /> +) + +Image.defaultProps = LazyLoadImage.defaultProps + +export default Image
\ No newline at end of file diff --git a/src/core/components/elements/Link/Link.jsx b/src/core/components/elements/Link/Link.jsx new file mode 100644 index 00000000..a619164d --- /dev/null +++ b/src/core/components/elements/Link/Link.jsx @@ -0,0 +1,17 @@ +import NextLink from "next/link" + +const Link = ({ children, ...props }) => { + return ( + <NextLink + {...props} + scroll={false} + className={`block font-medium text-red_r-11 ${props?.className}`} + > + {children} + </NextLink> + ) +} + +Link.defaultProps = NextLink.defaultProps + +export default Link
\ No newline at end of file diff --git a/src/core/components/elements/NavBar/NavBar.jsx b/src/core/components/elements/NavBar/NavBar.jsx new file mode 100644 index 00000000..212fd341 --- /dev/null +++ b/src/core/components/elements/NavBar/NavBar.jsx @@ -0,0 +1,31 @@ +import Image from "next/image" +import IndoteknikLogo from "@/images/logo.png" +import { Bars3Icon, HeartIcon, ShoppingCartIcon } from "@heroicons/react/24/outline" +import Link from "../Link/Link" +import Search from "./Search" + +const NavBar = () => { + return ( + <nav className="px-4 py-2 pb-3 sticky top-0 z-50 bg-white shadow"> + <div className="flex justify-between items-center mb-2"> + <Link href="/"> + <Image src={IndoteknikLogo} alt="Indoteknik Logo" width={120} height={40} /> + </Link> + <div className="flex gap-x-3"> + <button type="button"> + <HeartIcon className="w-6 text-gray_r-12" /> + </button> + <Link href="/shop/cart"> + <ShoppingCartIcon className="w-6 text-gray_r-12" /> + </Link> + <button type="button"> + <Bars3Icon className="w-6 text-gray_r-12" /> + </button> + </div> + </div> + <Search /> + </nav> + ) +} + +export default NavBar
\ No newline at end of file diff --git a/src/core/components/elements/NavBar/Search.jsx b/src/core/components/elements/NavBar/Search.jsx new file mode 100644 index 00000000..cca1a97c --- /dev/null +++ b/src/core/components/elements/NavBar/Search.jsx @@ -0,0 +1,89 @@ +import searchSuggestApi from "@/core/api/searchSuggestApi" +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline" +import { useCallback, useEffect, useRef, useState } from "react" +import Link from "../Link/Link" +import { useRouter } from "next/router" + +const Search = () => { + const router = useRouter() + const queryRef = useRef() + const [ query, setQuery ] = useState('') + const [ suggestions, setSuggestions ] = useState([]) + + useEffect(() => { + setQuery(router.query.q) + }, [router.query]) + + const loadSuggestion = useCallback(() => { + if (query && document.activeElement == queryRef.current) { + (async () => { + const dataSuggestion = await searchSuggestApi({ query }) + setSuggestions(dataSuggestion.data.suggestions) + })() + return + } else { + setSuggestions([]) + } + }, [ query ]) + + useEffect(() => { + if (query && document.activeElement == queryRef.current) { + loadSuggestion() + } else { + setSuggestions([]) + } + }, [ loadSuggestion, query ]) + + const handleSubmit = (e) => { + e.preventDefault() + if (query) { + router.push(`/shop/search?q=${query}`) + } else { + queryRef.current.focus() + } + } + + const onInputBlur = () => { + setTimeout(() => { + setSuggestions([]) + }, 100) + } + + return ( + <form + onSubmit={handleSubmit} + className="flex relative" + > + <input + type="text" + ref={queryRef} + className="form-input p-3 rounded-r-none border-r-0 focus:border-gray_r-6" + placeholder="Ketik nama, part number, merk" + value={query} + onChange={(e) => setQuery(e.target.value)} + onBlur={onInputBlur} + onFocus={loadSuggestion} + /> + <button + type="submit" + className="rounded-r border border-l-0 border-gray_r-6 px-2" + > + <MagnifyingGlassIcon className="w-6" /> + </button> + + { suggestions.length > 1 && ( + <> + <div className="absolute w-full top-[50px] rounded-b bg-gray_r-1 border border-gray_r-6 divide-y divide-gray_r-6"> + {suggestions.map((suggestion, index) => ( + <Link href={`/shop/search?q=${suggestion.term}`} key={index} className="px-3 py-3 !text-gray_r-12 font-normal"> + {suggestion.term} + </Link> + ))} + </div> + </> + ) } + </form> + ) +} + +export default Search
\ No newline at end of file diff --git a/src/core/components/elements/Pagination/Pagination.js b/src/core/components/elements/Pagination/Pagination.js new file mode 100644 index 00000000..485295fe --- /dev/null +++ b/src/core/components/elements/Pagination/Pagination.js @@ -0,0 +1,64 @@ +import Link from "../Link/Link" + +const Pagination = ({ pageCount, currentPage, url, className }) => { + let firstPage = false + let lastPage = false + let dotsPrevPage = false + let dotsNextPage = false + let urlParameterPrefix = url.includes('?') ? '&' : '?' + + return pageCount > 1 && ( + <div className={`pagination ${className}`}> + { 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> + ) +} + +Pagination.defaultProps = { + className: '' +} + +export default Pagination
\ No newline at end of file diff --git a/src/core/components/elements/Popup/BottomPopup.jsx b/src/core/components/elements/Popup/BottomPopup.jsx new file mode 100644 index 00000000..e687cf20 --- /dev/null +++ b/src/core/components/elements/Popup/BottomPopup.jsx @@ -0,0 +1,21 @@ +import { XMarkIcon } from "@heroicons/react/24/outline" + +const BottomPopup = ({ children, active, title, close }) => ( + <> + <div + onClick={close} + className={`overlay ${active ? 'block' : 'hidden'}`} + /> + <div className={`fixed bottom-0 left-0 w-full border-t border-gray_r-6 rounded-t-xl z-[60] p-4 pt-0 bg-white ${active ? 'block' : 'hidden'}`}> + <div className="flex justify-between py-4"> + <div className="font-semibold text-h-sm">{ title }</div> + <button type="button" onClick={close}> + <XMarkIcon className="w-5 stroke-2" /> + </button> + </div> + { children } + </div> + </> +) + +export default BottomPopup
\ No newline at end of file diff --git a/src/core/components/elements/Skeleton/BrandSkeleton.jsx b/src/core/components/elements/Skeleton/BrandSkeleton.jsx new file mode 100644 index 00000000..ce5a994d --- /dev/null +++ b/src/core/components/elements/Skeleton/BrandSkeleton.jsx @@ -0,0 +1,8 @@ +const BrandSkeleton = () => ( + <div role="status" className="animate-pulse"> + <div className="h-12 bg-gray-200 rounded"></div> + <span className="sr-only">Loading...</span> + </div> +) + +export default BrandSkeleton
\ No newline at end of file diff --git a/src/core/components/elements/Skeleton/ImageSkeleton.jsx b/src/core/components/elements/Skeleton/ImageSkeleton.jsx new file mode 100644 index 00000000..2cda9536 --- /dev/null +++ b/src/core/components/elements/Skeleton/ImageSkeleton.jsx @@ -0,0 +1,10 @@ +const ImageSkeleton = () => ( + <div role="status" className="animate-pulse"> + <div className="flex items-center justify-center h-56 mb-4 bg-gray-300 rounded" aria-busy> + <svg className="w-12 h-12 text-gray-200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" viewBox="0 0 640 512"><path d="M480 80C480 35.82 515.8 0 560 0C604.2 0 640 35.82 640 80C640 124.2 604.2 160 560 160C515.8 160 480 124.2 480 80zM0 456.1C0 445.6 2.964 435.3 8.551 426.4L225.3 81.01C231.9 70.42 243.5 64 256 64C268.5 64 280.1 70.42 286.8 81.01L412.7 281.7L460.9 202.7C464.1 196.1 472.2 192 480 192C487.8 192 495 196.1 499.1 202.7L631.1 419.1C636.9 428.6 640 439.7 640 450.9C640 484.6 612.6 512 578.9 512H55.91C25.03 512 .0006 486.1 .0006 456.1L0 456.1z"/></svg> + </div> + <span className="sr-only">Loading...</span> + </div> +) + +export default ImageSkeleton
\ No newline at end of file diff --git a/src/core/components/elements/Skeleton/ProductCardSkeleton.jsx b/src/core/components/elements/Skeleton/ProductCardSkeleton.jsx new file mode 100644 index 00000000..66b48f79 --- /dev/null +++ b/src/core/components/elements/Skeleton/ProductCardSkeleton.jsx @@ -0,0 +1,15 @@ +const ProductCardSkeleton = () => ( + <div role="status" className="p-4 max-w-sm rounded border border-gray-300 shadow animate-pulse md:p-6"> + <div className="flex items-center justify-center h-36 mb-4 bg-gray-300 rounded" aria-busy> + <svg className="w-12 h-12 text-gray-200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" viewBox="0 0 640 512"><path d="M480 80C480 35.82 515.8 0 560 0C604.2 0 640 35.82 640 80C640 124.2 604.2 160 560 160C515.8 160 480 124.2 480 80zM0 456.1C0 445.6 2.964 435.3 8.551 426.4L225.3 81.01C231.9 70.42 243.5 64 256 64C268.5 64 280.1 70.42 286.8 81.01L412.7 281.7L460.9 202.7C464.1 196.1 472.2 192 480 192C487.8 192 495 196.1 499.1 202.7L631.1 419.1C636.9 428.6 640 439.7 640 450.9C640 484.6 612.6 512 578.9 512H55.91C25.03 512 .0006 486.1 .0006 456.1L0 456.1z"/></svg> + </div> + <div className="h-2 bg-gray-200 rounded-full w-10 mb-1"></div> + <div className="h-2.5 bg-gray-200 rounded-full w-full mb-4"></div> + <div className="h-2 bg-gray-200 rounded-full mb-2.5"></div> + <div className="h-2 bg-gray-200 rounded-full mb-2.5"></div> + <div className="h-2 bg-gray-200 rounded-full"></div> + <span className="sr-only">Loading...</span> + </div> +) + +export default ProductCardSkeleton
\ No newline at end of file |
