From f99e0aba70efad0deb907d8e27f09fc9f527c8a4 Mon Sep 17 00:00:00 2001 From: Rafi Zadanly Date: Fri, 17 Feb 2023 17:07:50 +0700 Subject: Refactor --- src2/components/auth/WithAuth.js | 20 ++ src2/components/elements/Alert.js | 19 ++ src2/components/elements/BottomPopup.js | 25 ++ src2/components/elements/ConfirmAlert.js | 27 +++ src2/components/elements/DescriptionRow.js | 10 + src2/components/elements/Disclosure.js | 14 ++ src2/components/elements/Fields.js | 21 ++ src2/components/elements/Filter.js | 176 ++++++++++++++ src2/components/elements/Image.js | 17 ++ src2/components/elements/LineDivider.js | 7 + src2/components/elements/Link.js | 13 ++ src2/components/elements/Pagination.js | 58 +++++ src2/components/elements/ProgressBar.js | 25 ++ src2/components/elements/Skeleton.js | 48 ++++ src2/components/elements/Spinner.js | 13 ++ src2/components/layouts/AppBar.js | 47 ++++ src2/components/layouts/Footer.js | 91 ++++++++ src2/components/layouts/Header.js | 253 +++++++++++++++++++++ src2/components/layouts/Layout.js | 20 ++ src2/components/manufactures/ManufactureCard.js | 18 ++ src2/components/products/ProductCard.js | 69 ++++++ src2/components/products/ProductCategories.js | 62 +++++ src2/components/products/ProductSimilar.js | 25 ++ src2/components/products/ProductSlider.js | 39 ++++ src2/components/transactions/TransactionDetail.js | 67 ++++++ .../transactions/TransactionStatusBadge.js | 45 ++++ src2/components/variants/VariantCard.js | 92 ++++++++ src2/components/variants/VariantGroupCard.js | 31 +++ 28 files changed, 1352 insertions(+) create mode 100644 src2/components/auth/WithAuth.js create mode 100644 src2/components/elements/Alert.js create mode 100644 src2/components/elements/BottomPopup.js create mode 100644 src2/components/elements/ConfirmAlert.js create mode 100644 src2/components/elements/DescriptionRow.js create mode 100644 src2/components/elements/Disclosure.js create mode 100644 src2/components/elements/Fields.js create mode 100644 src2/components/elements/Filter.js create mode 100644 src2/components/elements/Image.js create mode 100644 src2/components/elements/LineDivider.js create mode 100644 src2/components/elements/Link.js create mode 100644 src2/components/elements/Pagination.js create mode 100644 src2/components/elements/ProgressBar.js create mode 100644 src2/components/elements/Skeleton.js create mode 100644 src2/components/elements/Spinner.js create mode 100644 src2/components/layouts/AppBar.js create mode 100644 src2/components/layouts/Footer.js create mode 100644 src2/components/layouts/Header.js create mode 100644 src2/components/layouts/Layout.js create mode 100644 src2/components/manufactures/ManufactureCard.js create mode 100644 src2/components/products/ProductCard.js create mode 100644 src2/components/products/ProductCategories.js create mode 100644 src2/components/products/ProductSimilar.js create mode 100644 src2/components/products/ProductSlider.js create mode 100644 src2/components/transactions/TransactionDetail.js create mode 100644 src2/components/transactions/TransactionStatusBadge.js create mode 100644 src2/components/variants/VariantCard.js create mode 100644 src2/components/variants/VariantGroupCard.js (limited to 'src2/components') diff --git a/src2/components/auth/WithAuth.js b/src2/components/auth/WithAuth.js new file mode 100644 index 00000000..ef975873 --- /dev/null +++ b/src2/components/auth/WithAuth.js @@ -0,0 +1,20 @@ +import { getAuth } from "@/core/utils/auth"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; + +const WithAuth = ({ children }) => { + const router = useRouter(); + const [response, setResponse] = useState(<>); + + useEffect(() => { + if (!getAuth()) { + router.replace('/login'); + } else { + setResponse(children); + } + }, [children, router]); + + return response; +} + +export default WithAuth; \ No newline at end of file diff --git a/src2/components/elements/Alert.js b/src2/components/elements/Alert.js new file mode 100644 index 00000000..914d1590 --- /dev/null +++ b/src2/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 ( +
{children}
+ ); +} + +export default Alert; \ No newline at end of file diff --git a/src2/components/elements/BottomPopup.js b/src2/components/elements/BottomPopup.js new file mode 100644 index 00000000..c1a56e10 --- /dev/null +++ b/src2/components/elements/BottomPopup.js @@ -0,0 +1,25 @@ +import CloseIcon from "@/icons/close.svg"; + +const BottomPopup = ({ + active = false, + title, + children, + closePopup = () => {} +}) => { + return ( + <> +
+
+
+

{ title }

+ +
+ { children } +
+ + ); +}; + +export default BottomPopup; \ No newline at end of file diff --git a/src2/components/elements/ConfirmAlert.js b/src2/components/elements/ConfirmAlert.js new file mode 100644 index 00000000..d33abb89 --- /dev/null +++ b/src2/components/elements/ConfirmAlert.js @@ -0,0 +1,27 @@ +const ConfirmAlert = ({ + title, + caption, + show, + onClose, + onSubmit, + closeText, + submitText +}) => { + return ( + <> + {show && ( +
+ )} +
+

{title}

+

{caption}

+
+ + +
+
+ + ); +}; + +export default ConfirmAlert; \ No newline at end of file diff --git a/src2/components/elements/DescriptionRow.js b/src2/components/elements/DescriptionRow.js new file mode 100644 index 00000000..7fe9e3a1 --- /dev/null +++ b/src2/components/elements/DescriptionRow.js @@ -0,0 +1,10 @@ +const DescriptionRow = ({ label, children }) => ( +
+

{ label }

+
+ { children } +
+
+); + +export default DescriptionRow; \ No newline at end of file diff --git a/src2/components/elements/Disclosure.js b/src2/components/elements/Disclosure.js new file mode 100644 index 00000000..1f334be3 --- /dev/null +++ b/src2/components/elements/Disclosure.js @@ -0,0 +1,14 @@ +const { ChevronUpIcon, ChevronDownIcon } = require("@heroicons/react/24/outline"); + +const Disclosure = ({ label, active, onClick }) => ( +
+

{ label }

+ { onClick && ( active ? ( + + ) : ( + + ) ) } +
+); + +export default Disclosure; \ No newline at end of file diff --git a/src2/components/elements/Fields.js b/src2/components/elements/Fields.js new file mode 100644 index 00000000..586a6a22 --- /dev/null +++ b/src2/components/elements/Fields.js @@ -0,0 +1,21 @@ +import ReactSelect from "react-select"; + +const Select = ({ + field, + ...props +}) => ( + <> + 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/src2/components/elements/Filter.js b/src2/components/elements/Filter.js new file mode 100644 index 00000000..f2051ba8 --- /dev/null +++ b/src2/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 ( + <> + +
+ {(selectedBrand || selectedCategory || priceFrom || priceTo || orderBy) && ( + + )} + + {!disableFilter.includes('orderBy') && ( +
+ +
+ {sortOptions.map((sortOption, index) => ( + + ))} +
+
+ )} + + {!disableFilter.includes('category') && ( +
+ + +
+ )} + + {!disableFilter.includes('brand') && ( +
+ + +
+ )} + + {!disableFilter.includes('price') && ( +
+ +
+ setPriceFrom(e.target.value)}/> + + setPriceTo(e.target.value)}/> +
+
+ )} + +
+
+ + ) +}; + +export default Filter; \ No newline at end of file diff --git a/src2/components/elements/Image.js b/src2/components/elements/Image.js new file mode 100644 index 00000000..60e249b9 --- /dev/null +++ b/src2/components/elements/Image.js @@ -0,0 +1,17 @@ +import { LazyLoadImage } from "react-lazy-load-image-component" +import "react-lazy-load-image-component/src/effects/opacity.css" + +const Image = ({ ...props }) => { + return ( + + ) +} + +Image.defaultProps = LazyLoadImage.defaultProps + +export default Image \ No newline at end of file diff --git a/src2/components/elements/LineDivider.js b/src2/components/elements/LineDivider.js new file mode 100644 index 00000000..4e8c7b52 --- /dev/null +++ b/src2/components/elements/LineDivider.js @@ -0,0 +1,7 @@ +const LineDivider = () => { + return ( +
+ ); +}; + +export default LineDivider; \ No newline at end of file diff --git a/src2/components/elements/Link.js b/src2/components/elements/Link.js new file mode 100644 index 00000000..065b5c9e --- /dev/null +++ b/src2/components/elements/Link.js @@ -0,0 +1,13 @@ +import NextLink from "next/link"; + +const Link = ({ children, ...props }) => { + return ( + + {children} + + ) +} + +Link.defaultProps = NextLink.defaultProps + +export default Link \ No newline at end of file diff --git a/src2/components/elements/Pagination.js b/src2/components/elements/Pagination.js new file mode 100644 index 00000000..ff2a8462 --- /dev/null +++ b/src2/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 pageCount > 1 && ( +
+ {Array.from(Array(pageCount)).map((v, i) => { + let page = i + 1; + let rangePrevPage = currentPage - 2; + let rangeNextPage = currentPage + 2; + let PageComponent = {page}; + let DotsComponent =
...
; + + 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; + } + })} +
+ ) +} \ No newline at end of file diff --git a/src2/components/elements/ProgressBar.js b/src2/components/elements/ProgressBar.js new file mode 100644 index 00000000..0adedcdf --- /dev/null +++ b/src2/components/elements/ProgressBar.js @@ -0,0 +1,25 @@ +import { Fragment } from "react"; + +const ProgressBar = ({ current, labels }) => { + return ( +
+ {labels.map((label, index) => ( + +
+
+ { index + 1 } +
+

{ label }

+
+ { index < (labels.length - 1) && ( +
+
+
+ ) } +
+ ))} +
+ ) +} + +export default ProgressBar; \ No newline at end of file diff --git a/src2/components/elements/Skeleton.js b/src2/components/elements/Skeleton.js new file mode 100644 index 00000000..fbdbc245 --- /dev/null +++ b/src2/components/elements/Skeleton.js @@ -0,0 +1,48 @@ +import ImagePlaceholderIcon from "../../icons/image-placeholder.svg"; + +const SkeletonList = ({ number }) => ( +
+ { Array.from(Array(number), (e, i) => ( +
+
+
+
+
+
+
+ )) } + Loading... +
+); + +const SkeletonProduct = () => ( +
+
+
+ +
+
+
+
+
+
+ Loading... +
+
+
+ +
+
+
+
+
+
+ Loading... +
+
+); + +export { + SkeletonList, + SkeletonProduct +}; \ No newline at end of file diff --git a/src2/components/elements/Spinner.js b/src2/components/elements/Spinner.js new file mode 100644 index 00000000..21006ecd --- /dev/null +++ b/src2/components/elements/Spinner.js @@ -0,0 +1,13 @@ +const Spinner = ({ className }) => { + return ( +
+ + Loading... +
+ ) +} + +export default Spinner; \ No newline at end of file diff --git a/src2/components/layouts/AppBar.js b/src2/components/layouts/AppBar.js new file mode 100644 index 00000000..fe74c940 --- /dev/null +++ b/src2/components/layouts/AppBar.js @@ -0,0 +1,47 @@ +import { Bars3Icon, ChevronLeftIcon, HomeIcon, ShoppingCartIcon } from "@heroicons/react/24/outline"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import Link from "../elements/Link"; + +const AppBar = ({ title }) => { + const router = useRouter(); + + const handleBackButtonClick = (event) => { + event.currentTarget.disabled = true; + router.back(); + } + + return ( + <> + + { title } - Indoteknik + +
+ {/* --- Start Title */} +
+ +

{ title }

+
+ {/* --- End Title */} + + {/* --- Start Icons */} +
+ + + + + + + + + +
+ {/* --- End Icons */} +
+ + ); +}; + +export default AppBar; \ No newline at end of file diff --git a/src2/components/layouts/Footer.js b/src2/components/layouts/Footer.js new file mode 100644 index 00000000..d173a525 --- /dev/null +++ b/src2/components/layouts/Footer.js @@ -0,0 +1,91 @@ +import { + PhoneIcon, + DevicePhoneMobileIcon, + EnvelopeIcon +} from "@heroicons/react/24/outline"; +import Image from "next/image"; +import InstagramIcon from "@/icons/instagram.svg"; +import LinkedinIcon from "@/icons/linkedin.svg"; +import Link from "../elements/Link"; + +export default function Footer() { + return ( +
+
+
+

Kantor Pusat

+

+ Jl. Bandengan Utara 85A No. 8-9 RT.3/RW.16, Penjaringan, Kec. Penjaringan +

+ +

Layanan Informasi

+ + + + +

Panduan Pelanggan

+
+ FAQ + Kebijakan Privasi + Pengajuan Tempo + Garansi Produk + Online Quotation + Pengiriman + Pembayaran + Syarat & Ketentuan + +
+
+
+

Jam Operasional

+

+ Senin - Jumat: 08:30 - 17:00 +

+

+ Sabtu: 08:30 - 14:00 +

+ +

Temukan Kami

+
+ + +
+ +

Pembayaran

+
+ BCA Payment + BCA Payment + BCA Payment + BCA Payment + BCA Payment + BCA Payment + BCA Payment + BCA Payment +
+ + {/*

Pengiriman

*/} +
+
+
PT. Indoteknik Dotcom Gemilang
+
+ ); +} \ No newline at end of file diff --git a/src2/components/layouts/Header.js b/src2/components/layouts/Header.js new file mode 100644 index 00000000..23fda642 --- /dev/null +++ b/src2/components/layouts/Header.js @@ -0,0 +1,253 @@ +import Image from "next/image"; +import { Fragment, useCallback, useEffect, useRef, useState } from "react"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import axios from "axios"; +import { + MagnifyingGlassIcon, + Bars3Icon, + ShoppingCartIcon, + ChevronRightIcon, + Cog6ToothIcon, + HeartIcon, + ChevronDownIcon, + ChevronUpIcon +} from "@heroicons/react/24/outline"; + +// Helpers +import { useAuth } from "@/core/utils/auth"; +// Components +import Link from "../elements/Link"; +// Images +import Logo from "@/images/logo.png"; +import greeting from "@/core/utils/greeting"; +import apiOdoo from "@/core/utils/apiOdoo"; + +const menus = [ + { name: 'Semua Brand', href: '/shop/brands' }, + { name: 'Blog Indoteknik', href: '/' }, + { name: 'Tentang Indoteknik', href: '/' }, + { name: 'Pusat Bantuan', href: '/' }, +]; + +export default function Header({ title }) { + const router = useRouter(); + const { q = '' } = router.query; + const [searchQuery, setSearchQuery] = useState(q != '*' ? q : ''); + const [suggestions, setSuggestions] = useState([]); + const searchQueryRef = useRef(); + const [isMenuActive, setIsMenuActive] = useState(false); + const [auth] = useAuth(); + + useEffect(() => { + if (q) { + searchQueryRef.current.blur(); + setSuggestions([]); + }; + }, [q]); + + const clickSuggestion = (value) => { + router.push(`/shop/search?q=${value}`, undefined, { scroll: false }); + }; + + const getSuggestion = useCallback(async () => { + if (searchQuery.trim().length > 0) { + let result = await axios(`${process.env.SELF_HOST}/api/shop/suggest?q=${searchQuery.trim()}`); + setSuggestions(result.data.suggest.mySuggester[searchQuery.trim()].suggestions); + } else { + setSuggestions([]); + } + }, [searchQuery]); + + useEffect(() => { + if (document.activeElement == searchQueryRef.current) getSuggestion(); + }, [getSuggestion]); + + const openMenu = () => setIsMenuActive(true); + const closeMenu = () => setIsMenuActive(false); + + const searchSubmit = (e) => { + e.preventDefault(); + if (searchQuery.length > 0) { + router.push(`/shop/search?q=${searchQuery}`, undefined, { scroll: false }); + } else { + searchQueryRef.current.focus(); + } + } + + const [ isOpenCategory, setOpenCategory ] = useState(false); + const [ categories, setCategories ] = useState([]); + + useEffect(() => { + const loadCategories = async () => { + if (isOpenCategory && categories.length == 0) { + let dataCategories = await apiOdoo('GET', '/api/v1/category/tree'); + dataCategories = dataCategories.map((category) => { + category.childs = category.childs.map((child1Category) => { + return { + ...child1Category, + isOpen: false + } + }) + return { + ...category, + isOpen: false + } + }); + setCategories(dataCategories); + } + } + loadCategories(); + }, [ isOpenCategory, categories ]); + + const toggleCategories = (id = 0) => { + let newCategories = categories.map((category) => { + category.childs = category.childs.map((child1Category) => { + return { + ...child1Category, + isOpen: id == child1Category.id ? !child1Category.isOpen : child1Category.isOpen + } + }) + return { + ...category, + isOpen: id == category.id ? !category.isOpen : category.isOpen + } + }); + setCategories(newCategories); + } + + return ( + <> + + + {title} + + +
+
+ { auth && ( + +
+

{ greeting() },

+

{auth.name}

+
+
+ +
+ + ) } + + { !auth && ( + <> + Masuk + Daftar + + ) } +
+
+ { menus.map((menu, index) => ( + + { menu.name } +
+ +
+ + )) } +
setOpenCategory(!isOpenCategory)}> + Kategori +
+ { !isOpenCategory && } + { isOpenCategory && } +
+
+ { isOpenCategory && categories.map((category) => ( + +
+ + { category.name } + +
toggleCategories(category.id)}> + { !category.isOpen && } + { category.isOpen && } +
+
+ { category.isOpen && category.childs.map((child1Category) => ( + +
+ + { child1Category.name } + + { child1Category.childs.length > 0 && ( +
toggleCategories(child1Category.id)}> + { !child1Category.isOpen && } + { child1Category.isOpen && } +
+ ) } +
+ { child1Category.isOpen && child1Category.childs.map((child2Category) => ( + + { child2Category.name } + + )) } +
+ )) } +
+ )) } +
+
+ + +
+
+ + Logo Indoteknik + +
+ + + + + + + +
+
+
+ setSearchQuery(e.target.value)} + onFocus={getSuggestion} + value={searchQuery} + className="form-input rounded-r-none border-r-0 focus:border-gray_r-7" + placeholder="Ketikan nama, merek, part number" + autoComplete="off" + /> + + + + {suggestions.length > 1 && ( +
+ {suggestions.map((suggestion, index) => ( +

clickSuggestion(suggestion.term)} className="w-full p-2" key={index}>{suggestion.term}

+ ))} +
+ )} +
+
+ + {suggestions.length > 1 && ( +
setSuggestions([])}>
+ )} + + ) +} \ No newline at end of file diff --git a/src2/components/layouts/Layout.js b/src2/components/layouts/Layout.js new file mode 100644 index 00000000..fd507963 --- /dev/null +++ b/src2/components/layouts/Layout.js @@ -0,0 +1,20 @@ +import { motion } from 'framer-motion'; + +export default function Layout({ children, ...pageProps }) { + const transition = { + ease: 'easeOut', + duration: 0.3 + }; + + return children && ( + + {children} + + ); +} \ No newline at end of file diff --git a/src2/components/manufactures/ManufactureCard.js b/src2/components/manufactures/ManufactureCard.js new file mode 100644 index 00000000..73a96902 --- /dev/null +++ b/src2/components/manufactures/ManufactureCard.js @@ -0,0 +1,18 @@ +import { createSlug } from "@/core/utils/slug"; +import Image from "../elements/Image"; +import Link from "../elements/Link"; + +export default function ManufactureCard({ data }) { + const manufacture = data; + return ( + + {manufacture.logo ? ( + {manufacture.name} + ) : manufacture.name} + + ); +} \ No newline at end of file diff --git a/src2/components/products/ProductCard.js b/src2/components/products/ProductCard.js new file mode 100644 index 00000000..c79a4900 --- /dev/null +++ b/src2/components/products/ProductCard.js @@ -0,0 +1,69 @@ +import Link from "../elements/Link"; +import currencyFormat from "@/core/utils/currencyFormat"; +import { createSlug } from "@/core/utils/slug"; +import { ChevronRightIcon } from "@heroicons/react/20/solid"; +import Image from "../elements/Image"; + + +export default function ProductCard({ + data, + simpleProductTitleLine = false +}) { + let product = data; + return ( +
+ + {product.name} + {product.variant_total > 1 ? ( +
{product.variant_total} Varian
+ ) : ''} + +
+
+ {typeof product.manufacture.name !== "undefined" ? ( + {product.manufacture.name} + ) : ( + - + )} + + {product.name} + +
+
+ {product.lowest_price.discount_percentage > 0 ? ( +
+

{currencyFormat(product.lowest_price.price)}

+ {product.lowest_price.discount_percentage}% +
+ ) : ''} + + {product.lowest_price.price_discount > 0 ? ( +

+ {currencyFormat(product.lowest_price.price_discount)} +

+ ) : ( + + Tanya Harga + + )} + + {product.stock_total > 0 ? ( +
+
Ready Stock
+
{product.stock_total > 5 ? '> 5' : '< 5'}
+
+ ) : ''} +
+
+
+ ) +} \ No newline at end of file diff --git a/src2/components/products/ProductCategories.js b/src2/components/products/ProductCategories.js new file mode 100644 index 00000000..3b671f29 --- /dev/null +++ b/src2/components/products/ProductCategories.js @@ -0,0 +1,62 @@ +import { useEffect, useState } from "react"; +import ProductSlider from "./ProductSlider"; +import apiOdoo from "@/core/utils/apiOdoo"; +import { LazyLoadComponent } from "react-lazy-load-image-component"; +import { SkeletonProduct } from "../elements/Skeleton"; + +const ProductCategory = ({ id }) => { + const [ content, setContent ] = useState(null); + + useEffect(() => { + const loadContent = async () => { + if (!content) { + const dataContent = await apiOdoo('GET', `/api/v1/categories_homepage?id=${id}`); + setContent(dataContent[0]); + } + } + loadContent(); + }, [id, content]); + + return ( +
+ { content ? ( + + ) : } +
+ ); +} + +export default function ProductCategories() { + const [ contentIds, setContentIds ] = useState([]); + + useEffect(() => { + const getContentIds = async () => { + if (contentIds.length == 0) { + const dataContentIds = await apiOdoo('GET', '/api/v1/categories_homepage/ids'); + setContentIds(dataContentIds); + } + } + getContentIds(); + }, [ contentIds ]); + + return ( +
+ { contentIds.map((contentId) => ( + } key={contentId}> + + + )) } +
+ ) +} \ No newline at end of file diff --git a/src2/components/products/ProductSimilar.js b/src2/components/products/ProductSimilar.js new file mode 100644 index 00000000..9e2292cb --- /dev/null +++ b/src2/components/products/ProductSimilar.js @@ -0,0 +1,25 @@ +import apiOdoo from '@/core/utils/apiOdoo'; +import { useEffect, useState } from 'react'; +import ProductSlider from './ProductSlider'; + +export default function ProductSimilar({ productId }) { + const [similarProducts, setSimilarProducts] = useState(null); + + useEffect(() => { + const getSimilarProducts = async () => { + if (productId && !similarProducts) { + const dataSimilarProducts = await apiOdoo('GET', `/api/v1/product/${productId}/similar?limit=20`); + setSimilarProducts(dataSimilarProducts); + } + } + getSimilarProducts(); + }, [productId, similarProducts]); + + + return ( +
+

Kamu Mungkin Juga Suka

+ +
+ ) +} \ No newline at end of file diff --git a/src2/components/products/ProductSlider.js b/src2/components/products/ProductSlider.js new file mode 100644 index 00000000..662a6511 --- /dev/null +++ b/src2/components/products/ProductSlider.js @@ -0,0 +1,39 @@ +import { Swiper, SwiperSlide } from "swiper/react"; +import ProductCard from "./ProductCard"; +import "swiper/css"; +import Image from "../elements/Image"; +import Link from "../elements/Link"; +import { SkeletonProduct } from "../elements/Skeleton"; +import { useState } from "react"; + +export default function ProductSlider({ + products, + simpleProductTitleLine = false, + bannerMode = false +}) { + const [ activeIndex, setActiveIndex ] = useState(0); + const swiperSliderFirstMove = (swiper) => { + setActiveIndex(swiper.activeIndex); + }; + + return ( + <> + { bannerMode && ( + {products.banner.name} 0 ? 'opacity-0' : 'opacity-100')} /> + ) } + + { bannerMode && ( + + + + ) } + {products?.products?.map((product, index) => ( + + + + ))} + + { !products ? : ''} + + ) +} \ No newline at end of file diff --git a/src2/components/transactions/TransactionDetail.js b/src2/components/transactions/TransactionDetail.js new file mode 100644 index 00000000..295a4f9f --- /dev/null +++ b/src2/components/transactions/TransactionDetail.js @@ -0,0 +1,67 @@ +import { useState } from "react"; +import DescriptionRow from "../elements/DescriptionRow"; +import Disclosure from "../elements/Disclosure"; + +const DetailAddress = ({ address }) => { + const fullAddress = []; + if (address?.street) fullAddress.push(address.street); + if (address?.sub_district?.name) fullAddress.push(address.sub_district.name); + if (address?.district?.name) fullAddress.push(address.district.name); + if (address?.city?.name) fullAddress.push(address.city.name); + return ( +
+ { address?.name } + { address?.email || '-' } + { address?.mobile || '-' } + { fullAddress.join(', ') } +
+ ); +}; + +const TransactionDetailAddress = ({ transaction }) => { + const [ activeSection, setActiveSection ] = useState({ + purchase: false, + shipping: false, + invoice: false, + }); + + const toggleSection = ( name ) => { + setActiveSection({ + ...activeSection, + [name]: !activeSection[name] + }); + }; + + return ( +
+ toggleSection('purchase')} + /> + { activeSection.purchase && ( + + ) } + + toggleSection('shipping')} + /> + { activeSection.shipping && ( + + ) } + + toggleSection('invoice')} + /> + { activeSection.invoice && ( + + ) } +
+ ); +}; + +export { TransactionDetailAddress }; \ No newline at end of file diff --git a/src2/components/transactions/TransactionStatusBadge.js b/src2/components/transactions/TransactionStatusBadge.js new file mode 100644 index 00000000..f94fd3fd --- /dev/null +++ b/src2/components/transactions/TransactionStatusBadge.js @@ -0,0 +1,45 @@ +const TransactionStatusBadge = ({ status }) => { + let badgeProps = { + className: ['h-fit'], + text: '' + }; + switch (status) { + case 'cancel': + badgeProps.className.push('badge-solid-red'); + badgeProps.text = 'Pesanan Batal' + break; + case 'draft': + badgeProps.className.push('badge-red'); + badgeProps.text = 'Pending Quotation' + break; + case 'waiting': + badgeProps.className.push('badge-yellow'); + badgeProps.text = 'Pesanan diterima' + break; + case 'sale': + badgeProps.className.push('badge-yellow'); + badgeProps.text = 'Pesanan diproses' + break; + case 'shipping': + badgeProps.className.push('badge-green'); + badgeProps.text = 'Pesanan dikirim' + break; + case 'partial_shipping': + badgeProps.className.push('badge-green'); + badgeProps.text = 'Dikirim sebagian' + break; + case 'done': + badgeProps.className.push('badge-solid-green'); + badgeProps.text = 'Pesanan Selesai' + break; + } + badgeProps.className = badgeProps.className.join(' '); + + return ( +
+ { badgeProps.text } +
+ ) +}; + +export default TransactionStatusBadge; \ No newline at end of file diff --git a/src2/components/variants/VariantCard.js b/src2/components/variants/VariantCard.js new file mode 100644 index 00000000..a821480c --- /dev/null +++ b/src2/components/variants/VariantCard.js @@ -0,0 +1,92 @@ +import { createSlug } from "@/core/utils/slug"; +import Image from "../elements/Image"; +import Link from "../elements/Link"; +import currencyFormat from "@/core/utils/currencyFormat"; +import { useRouter } from "next/router"; +import { toast } from "react-hot-toast"; +import { createOrUpdateItemCart } from "@/core/utils/cart"; + +export default function VariantCard({ + data, + openOnClick = true, + buyMore = false +}) { + let product = data; + const router = useRouter(); + + const addItemToCart = () => { + toast.success('Berhasil menambahkan ke keranjang', { duration: 1500 }); + createOrUpdateItemCart(product.id, 1); + return; + }; + + const checkoutItem = () => { + router.push(`/shop/checkout?product_id=${product.id}&qty=${product.quantity}`); + } + + const Card = () => ( +
+
+ {product.parent.name} +
+
+

+ {product.parent.name} +

+

+ {product.code || '-'} + {product.attributes.length > 0 ? ` ・ ${product.attributes.join(', ')}` : ''} +

+
+ {product.price.discount_percentage > 0 && ( + <> +

{currencyFormat(product.price.price)}

+ {product.price.discount_percentage}% + + )} +

{currencyFormat(product.price.price_discount)}

+
+

+ {currencyFormat(product.price.price_discount)} × {product.quantity} Barang +

+

+ {currencyFormat(product.quantity * product.price.price_discount)} +

+
+
+ ); + + if (openOnClick) { + return ( + <> + + + + { buyMore && ( +
+ + +
+ ) } + + ); + } + + return ; +} \ No newline at end of file diff --git a/src2/components/variants/VariantGroupCard.js b/src2/components/variants/VariantGroupCard.js new file mode 100644 index 00000000..462c63cf --- /dev/null +++ b/src2/components/variants/VariantGroupCard.js @@ -0,0 +1,31 @@ +import { useState } from "react" +import VariantCard from "./VariantCard" + +export default function VariantGroupCard({ + variants, + ...props +}) { + const [ showAll, setShowAll ] = useState(false) + const variantsToShow = showAll ? variants : variants.slice(0, 2) + + return ( + <> + { variantsToShow?.map((variant, index) => ( + + )) } + { variants.length > 2 && ( + + ) } + + ) +} \ No newline at end of file -- cgit v1.2.3