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/pages/shop | |
| parent | 90e1edab9b6a8ccc09a49fed3addbec2cbc4e4c3 (diff) | |
Refactor
Diffstat (limited to 'src/pages/shop')
| -rw-r--r-- | src/pages/shop/brands/[slug].js | 178 | ||||
| -rw-r--r-- | src/pages/shop/brands/[slug].jsx | 23 | ||||
| -rw-r--r-- | src/pages/shop/brands/index.js | 79 | ||||
| -rw-r--r-- | src/pages/shop/cart.js | 282 | ||||
| -rw-r--r-- | src/pages/shop/cart.jsx | 10 | ||||
| -rw-r--r-- | src/pages/shop/checkout/finish.js | 47 | ||||
| -rw-r--r-- | src/pages/shop/checkout/index.js | 325 | ||||
| -rw-r--r-- | src/pages/shop/product/[slug].js | 305 | ||||
| -rw-r--r-- | src/pages/shop/product/[slug].jsx | 29 | ||||
| -rw-r--r-- | src/pages/shop/quotation/finish.js | 39 | ||||
| -rw-r--r-- | src/pages/shop/quotation/index.js | 140 | ||||
| -rw-r--r-- | src/pages/shop/search.js | 125 | ||||
| -rw-r--r-- | src/pages/shop/search.jsx | 19 |
13 files changed, 81 insertions, 1520 deletions
diff --git a/src/pages/shop/brands/[slug].js b/src/pages/shop/brands/[slug].js deleted file mode 100644 index a387e55d..00000000 --- a/src/pages/shop/brands/[slug].js +++ /dev/null @@ -1,178 +0,0 @@ -import axios from "axios"; -import { useEffect, useState } from "react"; -import Filter from "@/components/elements/Filter"; -import Footer from "@/components/layouts/Footer"; -import Header from "@/components/layouts/Header"; -import Layout from "@/components/layouts/Layout"; -import Pagination from "@/components/elements/Pagination"; -import ProductCard from "@/components/products/ProductCard"; -import { getIdFromSlug, getNameFromSlug } from "@/core/utils/slug"; -import FilterIcon from "@/icons/filter.svg"; -import apiOdoo from "@/core/utils/apiOdoo"; -import { Swiper, SwiperSlide } from "swiper/react"; -import "swiper/css"; -import "swiper/css/pagination"; -import "swiper/css/autoplay"; -import { Pagination as SwiperPagination } from "swiper"; -import Image from "@/components/elements/Image"; -import LineDivider from "@/components/elements/LineDivider"; - -export async function getServerSideProps(context) { - const { - slug, - page = 1, - category = '', - price_from = '', - price_to = '', - order_by = '', - } = context.query; - - let urlParameter = [ - 'q=*', - `page=${page}`, - `brand=${getNameFromSlug(slug)}`, - `category=${category}`, - `price_from=${price_from}`, - `price_to=${price_to}`, - `order_by=${order_by}` - ].join('&'); - let searchResults = await axios(`${process.env.SELF_HOST}/api/shop/search?${urlParameter}`); - searchResults = searchResults.data; - - const manufacture = await apiOdoo('GET', `/api/v1/manufacture/${getIdFromSlug(slug)}`); - - return { - props: { - searchResults, - page, - slug, - category, - price_from, - price_to, - order_by, - manufacture - } - }; -} - -export default function BrandDetail({ - searchResults, - page, - slug, - category, - price_from, - price_to, - order_by, - manufacture -}) { - const pageCount = Math.ceil(searchResults.response.numFound / searchResults.responseHeader.params.rows); - const productStart = searchResults.responseHeader.params.start; - const productRows = searchResults.responseHeader.params.rows; - const productFound = searchResults.response.numFound; - - const [activeFilter, setActiveFilter] = useState(false); - const [filterCount, setFilterCount] = useState(0); - - const route = () => { - let route = `/shop/brands/${slug}`; - if (category) route += `&category=${category}`; - if (price_from) route += `&price_from=${price_from}`; - if (price_to) route += `&price_to=${price_to}`; - if (order_by) route += `&order_by=${order_by}`; - return route; - } - - useEffect(() => { - let calculateFilterCount = 0; - if (category) calculateFilterCount++; - if (price_from || price_to) calculateFilterCount++; - if (order_by) calculateFilterCount++; - setFilterCount(calculateFilterCount); - }, [category, price_from, price_to, order_by]); - - return ( - <> - <Header title={`Distributor ${getNameFromSlug(slug)} Indonesia Harga Official - Indoteknik`} /> - <Filter - defaultRoute={`/shop/brands/${slug}`} - isActive={activeFilter} - closeFilter={() => setActiveFilter(false)} - defaultPriceFrom={price_from} - defaultPriceTo={price_to} - defaultBrand='' - defaultCategory={category} - defaultOrderBy={order_by} - searchResults={searchResults} - disableFilter={['brand']} - /> - <Layout> - <Swiper slidesPerView={1} pagination={{dynamicBullets: true}} modules={[SwiperPagination]}> - { - manufacture.banners?.map((banner, index) => ( - <SwiperSlide key={index}> - <Image - src={banner} - alt={`Banner ${manufacture.name}`} - className="w-full h-auto border-b border-gray_r-6" - /> - </SwiperSlide> - )) - } - </Swiper> - <div className="p-4 grid grid-cols-2"> - <div> - <p className="text-caption-2 text-gray_r-11 mb-2">Produk dari brand:</p> - { manufacture.logo ? ( - <div className="w-8/12"> - <Image src={manufacture?.logo} alt={manufacture.name} className="border border-gray_r-6 rounded p-3" /> - </div> - ) : ( - <p className="badge-solid-red text-caption-1">{ manufacture.name }</p> - ) } - </div> - <div className="text-right"> - <p className="text-caption-2 text-gray_r-11 mb-2">Jumlah Produk:</p> - <p>{ searchResults.response.numFound }</p> - </div> - </div> - - <LineDivider /> - - <div className="p-4"> - <h1 className="mb-2">Produk</h1> - <div className="text-caption-1 mb-4"> - {productFound > 0 ? ( - <> - Menampilkan - {pageCount > 1 ? ( - <> - {productStart + 1}-{ - (productStart + productRows) > productFound ? productFound : productStart + productRows - } - dari - </> - ) : ''} - {searchResults.response.numFound} - produk untuk brand <span className="font-semibold">{getNameFromSlug(slug)}</span> - </> - ) : 'Mungkin yang anda cari'} - </div> - <button className="btn-light py-2 flex items-center gap-x-2 mb-4" onClick={() => setActiveFilter(true)}> - <FilterIcon className="w-4 h-4" /> <span>Filter {filterCount > 0 ? `(${filterCount})` : ''}</span> - </button> - <div className="grid grid-cols-2 gap-3"> - {searchResults.response.products.map((product) => ( - <ProductCard key={product.id} data={product} /> - ))} - </div> - - <div className="mt-4"> - <Pagination pageCount={pageCount} currentPage={parseInt(page)} url={route()} /> - </div> - </div> - - <Footer /> - </Layout> - </> - ) -}
\ No newline at end of file diff --git a/src/pages/shop/brands/[slug].jsx b/src/pages/shop/brands/[slug].jsx new file mode 100644 index 00000000..4965d4f7 --- /dev/null +++ b/src/pages/shop/brands/[slug].jsx @@ -0,0 +1,23 @@ +import BasicLayout from "@/core/components/layouts/BasicLayout" +import { getIdFromSlug, getNameFromSlug } from "@/core/utils/slug" +import ProductSearch from "@/lib/product/components/ProductSearch" +import { useRouter } from "next/router" +import _ from "lodash" +import Brand from "@/lib/brand/components/Brand" + +export default function BrandDetail() { + const router = useRouter() + const { slug = '' } = router.query + return ( + <BasicLayout> + <Brand id={getIdFromSlug(slug)} /> + { !_.isEmpty(router.query) && ( + <ProductSearch + query={_.omit(router.query, 'slug')} + prefixUrl={`/shop/brands/${slug}`} + defaultBrand={getNameFromSlug(slug)} + /> + ) } + </BasicLayout> + ) +}
\ No newline at end of file diff --git a/src/pages/shop/brands/index.js b/src/pages/shop/brands/index.js deleted file mode 100644 index bfdcd403..00000000 --- a/src/pages/shop/brands/index.js +++ /dev/null @@ -1,79 +0,0 @@ -import Header from "@/components/layouts/Header"; -import apiOdoo from "@/core/utils/apiOdoo"; -import InfiniteScroll from "react-infinite-scroll-component"; -import { useCallback, useEffect, useState } from "react"; -import Spinner from "@/components/elements/Spinner"; -import Layout from "@/components/layouts/Layout"; -import ManufactureCard from "@/components/manufactures/ManufactureCard"; -import Footer from "@/components/layouts/Footer"; - -export async function getServerSideProps() { - let initialManufactures = await apiOdoo('GET', '/api/v1/manufacture?limit=31'); - return {props: {initialManufactures}}; -} - -export default function Brands({ initialManufactures }) { - const [manufactures, setManufactures] = useState(initialManufactures.manufactures); - const [hasMoreManufacture, setHasMoreManufacture] = useState(true); - const [manufactureStartwith, setManufactureStartWith] = useState(''); - - const alpha = Array.from(Array(26)).map((e, i) => i + 65); - const alphabets = alpha.map((x) => String.fromCharCode(x)); - - const getMoreManufactures = useCallback(async () => { - const name = manufactureStartwith != '' ? `${manufactureStartwith}%` : ''; - const result = await apiOdoo('GET', `/api/v1/manufacture?limit=30&offset=${manufactures.length}&name=${name}`); - setHasMoreManufacture(manufactures.length + 30 < result.manufacture_total) - setManufactures((manufactures) => [...manufactures, ...result.manufactures]); - }, [ manufactureStartwith ]); - - const filterManufactureStartWith = (character) => { - setManufactures([]); - if (manufactureStartwith == character) { - setManufactureStartWith(''); - } else { - setManufactureStartWith(character); - } - }; - - useEffect(() => { - getMoreManufactures(); - }, [ getMoreManufactures ]); - - return ( - <> - <Header title='Semua Brand di Indoteknik' /> - <Layout> - <div className="p-4"> - <h1>Semua Brand di Indoteknik</h1> - <div className="flex overflow-x-auto gap-x-2 py-2"> - {alphabets.map((alphabet, index) => ( - <button key={index} className={"p-2 py-1 border bg-white border-gray_r-6 rounded w-10 flex-shrink-0" + (manufactureStartwith == alphabet ? ' !bg-yellow_r-9 border-yellow_r-9 ' : '')} onClick={() => filterManufactureStartWith(alphabet)}> - {alphabet} - </button> - ))} - </div> - <InfiniteScroll - dataLength={manufactures.length} - next={getMoreManufactures} - hasMore={hasMoreManufacture} - className="grid grid-cols-4 gap-4 mt-6 !overflow-x-hidden" - loader={ - <div className="flex justify-center items-center border border-gray-300 p-2 rounded h-14"> - <Spinner className="w-6 h-6 text-gray-600 fill-gray-900"/> - </div> - } - > - {manufactures?.map((manufacture, index) => ( - manufacture.name ? ( - <ManufactureCard data={manufacture} key={index} /> - ) : '' - ))} - </InfiniteScroll> - </div> - - <Footer /> - </Layout> - </> - ) -}
\ No newline at end of file diff --git a/src/pages/shop/cart.js b/src/pages/shop/cart.js deleted file mode 100644 index 1178781b..00000000 --- a/src/pages/shop/cart.js +++ /dev/null @@ -1,282 +0,0 @@ -import { useEffect, useState } from "react"; -import { toast } from "react-hot-toast"; -import { - TrashIcon, - PlusIcon, - MinusIcon, - ExclamationCircleIcon, -} from "@heroicons/react/24/solid"; -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import { useRouter } from "next/router"; - -// Helpers -import { - createOrUpdateItemCart, - deleteItemCart, - getCart -} from "@/core/utils/cart"; -import { createSlug } from "@/core/utils/slug"; -import apiOdoo from "@/core/utils/apiOdoo"; -import currencyFormat from "@/core/utils/currencyFormat"; - -// Components -import Image from "@/components/elements/Image"; -import Layout from "@/components/layouts/Layout"; -import Link from "@/components/elements/Link"; -import Alert from "@/components/elements/Alert"; -import Spinner from "@/components/elements/Spinner"; -import AppBar from "@/components/layouts/AppBar"; -import ProgressBar from "@/components/elements/ProgressBar"; -import LineDivider from "@/components/elements/LineDivider"; -import useConfirmAlert from "@/lib/elements/hooks/useConfirmAlert"; - -export default function Cart() { - const router = useRouter(); - const [isLoadingProducts, setIsLoadingProducts] = useState(true); - const [products, setProducts] = useState([]); - const [totalPriceBeforeTax, setTotalPriceBeforeTax] = useState(0); - const [totalTaxAmount, setTotalTaxAmount] = useState(0); - const [totalDiscountAmount, setTotalDiscountAmount] = useState(0); - - useEffect(() => { - const getProducts = async () => { - let cart = getCart(); - let productIds = Object.keys(cart); - if (productIds.length > 0) { - productIds = productIds.join(','); - let dataProducts = await apiOdoo('GET', `/api/v1/product_variant/${productIds}`); - dataProducts = dataProducts.map((product) => ({ - ...product, - quantity: cart[product.id].quantity, - selected: cart[product.id].selected, - })); - setProducts(dataProducts); - } - setIsLoadingProducts(false); - } - getProducts(); - }, []); - - useEffect(() => { - for (const product of products) { - if (product.quantity != '') createOrUpdateItemCart(product.id, product.quantity, product.selected); - } - const productsSelected = products.filter((product) => product.selected == true); - let calculateTotalPriceBeforeTax = 0; - let calculateTotalTaxAmount = 0; - let calculateTotalDiscountAmount = 0; - productsSelected.forEach(product => { - let priceBeforeTax = product.price.price / 1.11; - calculateTotalPriceBeforeTax += priceBeforeTax * product.quantity; - calculateTotalTaxAmount += (product.price.price - priceBeforeTax) * product.quantity; - calculateTotalDiscountAmount += (product.price.price - product.price.price_discount) * product.quantity; - }); - setTotalPriceBeforeTax(calculateTotalPriceBeforeTax); - setTotalTaxAmount(calculateTotalTaxAmount); - setTotalDiscountAmount(calculateTotalDiscountAmount); - }, [products]); - - const getProductsSelected = () => { - return products.filter((product) => product.selected == true); - } - - const updateCart = (productId, quantity) => { - let productIndexToUpdate = products.findIndex((product) => product.id == productId); - let productsToUpdate = products; - productsToUpdate[productIndexToUpdate].quantity = quantity; - setProducts([...productsToUpdate]); - }; - - const blurQuantity = (productId, quantity) => { - quantity = quantity == ('' || 0) ? 1 : parseInt(quantity); - if (typeof quantity === 'number') { - quantity = parseInt(quantity); - quantity = Math.floor(quantity); - } - updateCart(productId, quantity); - }; - - const updateQuantity = (productId, quantity) => { - quantity = quantity == '' ? '' : parseInt(quantity); - updateCart(productId, quantity); - }; - - const plusQuantity = (productId) => { - let productIndexToUpdate = products.findIndex((product) => product.id == productId); - let quantity = products[productIndexToUpdate].quantity + 1; - updateCart(productId, quantity); - } - - const minusQuantity = (productId) => { - let productIndexToUpdate = products.findIndex((product) => product.id == productId); - let quantity = products[productIndexToUpdate].quantity - 1; - updateCart(productId, quantity); - } - - const toggleProductSelected = (productId) => { - let productIndexToUpdate = products.findIndex((product) => product.id == productId); - let productsToUpdate = products; - productsToUpdate[productIndexToUpdate].selected = !productsToUpdate[productIndexToUpdate].selected; - setProducts([...productsToUpdate]); - } - - const deleteItem = (productId) => { - let productIndexToUpdate = products.findIndex((product) => product.id == productId); - let productsToUpdate = products; - productsToUpdate.splice(productIndexToUpdate, 1); - setProducts([...productsToUpdate]); - deleteItemCart(productId); - toast.success('Berhasil menghapus 1 barang dari keranjang', { duration: 1500 }); - } - - const { - openConfirmAlert, - ConfirmAlert - } = useConfirmAlert({ - title: 'Hapus barang dari keranjang', - caption:'Apakah anda yakin menghapus barang dari keranjang?', - closeText: 'Batal', - submitText: 'Hapus', - onSubmit: deleteItem - }) - - return ( - <> - { ConfirmAlert } - - <Layout> - <AppBar title="Keranjang Saya" /> - - {isLoadingProducts && ( - <div className="flex justify-center items-center gap-x-3 mt-14"> - <Spinner className="w-10 text-gray_r-8 fill-gray_r-12" /> - </div> - ) } - - { !isLoadingProducts && products.length == 0 && ( - <div className="text-center mt-14"> - <ExclamationTriangleIcon className="w-12 mx-auto"/> - <p className="mt-2 h2">Keranjang belanja anda masih kosong.</p> - <Link href="/" className="btn-yellow text-gray_r-12 mx-auto mt-4">Mulai Belanja</Link> - </div> - ) } - - { !isLoadingProducts && products.length > 0 && ( - <> - <ProgressBar - current={1} - labels={['Keranjang', 'Pembayaran', 'Selesai']} - /> - - <LineDivider /> - - <div className="p-4"> - <Alert type="warning" className="text-caption-2 flex gap-x-3 items-center"> - <div> - <ExclamationCircleIcon className="w-8 text-yellow_r-11"/> - </div> - <span>Mohon dicek kembali & pastikan pesanan kamu sudah sesuai dengan yang kamu butuhkan. Atau bisa hubungi kami.</span> - </Alert> - </div> - - <LineDivider /> - - <div className="p-4 flex flex-col gap-y-6"> - <div className="flex justify-between items-center"> - <h2>Daftar Produk Belanja</h2> - <Link href="/" className="text-caption-1">Cari Produk Lain</Link> - </div> - {products.map((product, index) => ( - <div className="flex gap-x-3" key={index}> - <div className="w-4/12 flex items-center gap-x-2" onClick={() => toggleProductSelected(product.id)}> - <button - className={'p-2 rounded border-2 ' + (product.selected ? 'border-yellow_r-9 bg-yellow_r-9' : 'border-gray_r-12')} - ></button> - <Image - src={product.parent.image} - alt={product.parent.name} - className="object-contain object-center border border-gray_r-6 h-32 w-full rounded-md" - /> - </div> - <div className="w-8/12 flex flex-col"> - <Link href={'/shop/product/' + createSlug(product.parent.name, product.parent.id)} className="product-card__title wrap-line-ellipsis-2"> - {product.parent.name} - </Link> - <p className="text-caption-2 text-gray_r-11 mt-1"> - {product.code || '-'} - {product.attributes.length > 0 ? ` | ${product.attributes.join(', ')}` : ''} - </p> - <div className="flex flex-wrap gap-x-1 items-center mb-2 mt-auto"> - {product.price.discount_percentage > 0 && ( - <> - <p className="text-caption-2 text-gray_r-11 line-through">{currencyFormat(product.price.price)}</p> - <span className="badge-red">{product.price.discount_percentage}%</span> - </> - )} - <p className="text-caption-2 text-gray_r-12">{currencyFormat(product.price.price_discount)}</p> - </div> - <div className="flex items-center"> - <p className="mr-auto text-caption-2 text-gray_r-12 font-bold">{currencyFormat(product.quantity * product.price.price_discount)}</p> - <div className="flex gap-x-2 items-center"> - <button - className="btn-red p-2 rounded" - onClick={() => openConfirmAlert(product.id)} - > - <TrashIcon className="text-red_r-11 w-3"/> - </button> - <button - className="btn-light p-2 rounded" - disabled={product.quantity == 1} - onClick={() => minusQuantity(product.id)} - > - <MinusIcon className={'text-gray_r-12 w-3' + (product.quantity == 1 ? ' text-gray_r-11' : '')}/> - </button> - <input - type="number" - className="bg-transparent border-none w-6 text-center outline-none" - onBlur={(e) => blurQuantity(product.id, e.target.value)} - onChange={(e) => updateQuantity(product.id, e.target.value)} - value={product.quantity} - /> - <button className="btn-light p-2 rounded" onClick={() => plusQuantity(product.id)}> - <PlusIcon className="text-gray_r-12 w-3"/> - </button> - </div> - </div> - </div> - </div> - ))} - </div> - - <div className="p-4 bg-gray_r-1 sticky bottom-0 border-t-4 border-gray_r-4"> - <div className="flex"> - <p>Total</p> - <p className="text-gray_r-11 ml-1">{getProductsSelected().length > 0 && ( - <>({ getProductsSelected().length } Barang)</> - )}</p> - <p className="font-semibold text-red_r-11 ml-auto">{currencyFormat(totalPriceBeforeTax + totalTaxAmount - totalDiscountAmount)}</p> - </div> - - <div className="flex gap-x-3 mt-4"> - <button - className="flex-1 btn-light" - disabled={getProductsSelected().length == 0} - onClick={() => router.push('/shop/quotation')} - > - Quotation {getProductsSelected().length > 0 && `(${getProductsSelected().length})`} - </button> - <button - className="flex-1 btn-yellow" - disabled={getProductsSelected().length == 0} - onClick={() => router.push('/shop/checkout')} - > - Checkout {getProductsSelected().length > 0 && `(${getProductsSelected().length})`} - </button> - </div> - </div> - </> - ) } - </Layout> - </> - ); -}
\ No newline at end of file diff --git a/src/pages/shop/cart.jsx b/src/pages/shop/cart.jsx new file mode 100644 index 00000000..20279e0c --- /dev/null +++ b/src/pages/shop/cart.jsx @@ -0,0 +1,10 @@ +import AppLayout from "@/core/components/layouts/AppLayout" +import CartComponent from "@/lib/cart/components/Cart" + +export default function Cart() { + return ( + <AppLayout title="Keranjang"> + <CartComponent /> + </AppLayout> + ) +}
\ No newline at end of file diff --git a/src/pages/shop/checkout/finish.js b/src/pages/shop/checkout/finish.js deleted file mode 100644 index df284f8a..00000000 --- a/src/pages/shop/checkout/finish.js +++ /dev/null @@ -1,47 +0,0 @@ -import WithAuth from "@/components/auth/WithAuth"; -import Link from "@/components/elements/Link"; -import AppBar from "@/components/layouts/AppBar"; -import Header from "@/components/layouts/Header"; -import Layout from "@/components/layouts/Layout"; -import apiOdoo from "@/core/utils/apiOdoo"; -import { useAuth } from "@/core/utils/auth"; -import { EnvelopeIcon } from "@heroicons/react/24/outline"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; - -export default function FinishCheckout() { - const router = useRouter(); - const { id } = router.query; - const [ auth ] = useAuth(); - const [ transaction, setTransactions ] = useState(null); - - useEffect(() => { - const loadTransaction = async () => { - if (auth && id) { - const dataTransaction = await apiOdoo('GET', `/api/v1/partner/${auth.partner_id}/sale_order/${id}`); - setTransactions(dataTransaction); - } - }; - loadTransaction(); - }); - - return ( - <WithAuth> - <Layout> - <AppBar title="Pembelian Berhasil" /> - - <div className="m-4 rounded-xl bg-yellow_r-4 text-center border border-yellow_r-7"> - <div className="px-4 py-6 text-yellow_r-12"> - <p className="h2 mb-2">Terima Kasih atas Pembelian Anda</p> - <p className="text-yellow_r-11 mb-4 leading-6">Rincian belanja sudah kami kirimkan ke email anda. Mohon dicek kembali. jika tidak menerima email, anda dapat menghubungi kami disini.</p> - <p className="mb-2 font-medium">{ transaction?.name }</p> - <p className="text-caption-2 text-yellow_r-11">No. Transaksi</p> - </div> - <Link href={transaction?.id ? `/my/transaction/${transaction.id}` : '/'} className="bg-yellow_r-6 text-yellow_r-12 rounded-b-xl py-4 block"> - Lihat detail pembelian Anda disini - </Link> - </div> - </Layout> - </WithAuth> - ); -}
\ No newline at end of file diff --git a/src/pages/shop/checkout/index.js b/src/pages/shop/checkout/index.js deleted file mode 100644 index 0a77ebed..00000000 --- a/src/pages/shop/checkout/index.js +++ /dev/null @@ -1,325 +0,0 @@ -import { ExclamationCircleIcon } from "@heroicons/react/24/solid" -import { useEffect, useState } from "react" -import Alert from "@/components/elements/Alert" -import AppBar from "@/components/layouts/AppBar" -import Layout from "@/components/layouts/Layout" -import LineDivider from "@/components/elements/LineDivider" -import Link from "@/components/elements/Link" -import ProgressBar from "@/components/elements/ProgressBar" -import Spinner from "@/components/elements/Spinner" -import apiOdoo from "@/core/utils/apiOdoo" -import { useAuth } from "@/core/utils/auth" -import { deleteItemCart, getCart } from "@/core/utils/cart" -import currencyFormat from "@/core/utils/currencyFormat" -import { getItemAddress } from "@/core/utils/address" -import { useRouter } from "next/router" -import WithAuth from "@/components/auth/WithAuth" -import { toast } from "react-hot-toast" -import getFileBase64 from "@/core/utils/getFileBase64" -import VariantCard from "@/components/variants/VariantCard" - -export default function Checkout() { - const router = useRouter() - const { product_id, qty } = router.query - const [ auth ] = useAuth() - const [ addresses, setAddresses ] = useState(null) - const [ poNumber, setPoNumber ] = useState('') - const [ poFile, setPoFile ] = useState('') - const [ selectedAddress, setSelectedAddress ] = useState({ - shipping: null, - invoicing: null - }) - const [ selectedPayment, setSelectedPayment ] = useState(null) - const [ products, setProducts ] = useState(null) - const [ totalAmount, setTotalAmount ] = useState(0) - const [ totalDiscountAmount, setTotalDiscountAmount ] = useState(0) - - const [ isLoading, setIsLoading ] = useState(false) - - const payments = [ - { name: 'BCA', number: '8870-4000-81' }, - { name: 'MANDIRI', number: '155-0067-6869-75' }, - ] - - useEffect(() => { - const getAddresses = async () => { - if (auth) { - const dataAddresses = await apiOdoo('GET', `/api/v1/user/${auth.id}/address`) - setAddresses(dataAddresses) - } - } - getAddresses() - }, [auth]) - - useEffect(() => { - const getProducts = async () => { - let cart = getCart() - let productIds = [] - if (product_id) { - productIds = [parseInt(product_id)] - } else { - productIds = Object - .values(cart) - .filter((itemCart) => itemCart.selected == true) - .map((itemCart) => itemCart.product_id) - } - if (productIds.length > 0) { - productIds = productIds.join(',') - let dataProducts = await apiOdoo('GET', `/api/v1/product_variant/${productIds}`) - dataProducts = dataProducts.map((product) => { - if (product_id) { - product.quantity = 1 - if (qty) product.quantity = parseInt(qty) - } else { - product.quantity = cart[product.id].quantity - } - return product - }) - setProducts(dataProducts) - } - } - getProducts() - }, [router, auth, product_id, qty]) - - useEffect(() => { - if (addresses) { - const matchAddress = (key) => { - const addressToMatch = getItemAddress(key) - let foundAddress = addresses.filter((address) => address.id == addressToMatch) - if (foundAddress.length > 0) { - return foundAddress[0] - } - return addresses[0] - } - setSelectedAddress({ - shipping: matchAddress('shipping'), - invoicing: matchAddress('invoicing'), - }) - } - }, [addresses]) - - useEffect(() => { - if (products) { - let calculateTotalAmount = 0 - let calculateTotalDiscountAmount = 0 - products.forEach(product => { - calculateTotalAmount += product.price.price * product.quantity - calculateTotalDiscountAmount += (product.price.price - product.price.price_discount) * product.quantity - }) - setTotalAmount(calculateTotalAmount) - setTotalDiscountAmount(calculateTotalDiscountAmount) - } - }, [products]) - - const checkout = async () => { - if (!selectedPayment) { - toast.error('Mohon pilih metode pembayaran', { - position: 'bottom-center' - }) - return - } - if (poFile && poFile.size > 5000000) { - toast.error('Maksimal ukuran file adalah 5MB', { - position: 'bottom-center' - }) - return - } - setIsLoading(true) - let productOrder = products.map((product) => ({ 'product_id': product.id, 'quantity': product.quantity })) - let data = { - 'partner_shipping_id': selectedAddress.shipping.id, - 'partner_invoice_id': selectedAddress.invoicing.id, - 'order_line': JSON.stringify(productOrder), - 'type': 'sale_order' - } - if (poNumber) data.po_number = poNumber - if (poFile) data.po_file = await getFileBase64(poFile) - - const checkoutToOdoo = await apiOdoo('POST', `/api/v1/partner/${auth.partner_id}/sale_order/checkout`, data) - for (const product of products) { - deleteItemCart(product.id) - } - router.push(`/shop/checkout/finish?id=${checkoutToOdoo.id}`) - setIsLoading(false) - } - - return ( - <WithAuth> - <Layout> - <AppBar title={"Checkout"} /> - { !products && !addresses && ( - <div className="flex justify-center items-center gap-x-3 mt-14"> - <Spinner className="w-10 text-gray_r-8 fill-gray_r-12" /> - </div> - ) } - - { products && addresses && ( - <> - <ProgressBar - current={2} - labels={['Keranjang', 'Pembayaran', 'Selesai']} - /> - - <LineDivider/> - - <div className="p-4"> - <Alert type="info" className="text-caption-2 flex gap-x-3 items-center"> - <div> - <ExclamationCircleIcon className="w-6 text-blue-700"/> - </div> - <span>Jika mengalami kesulitan dalam melakukan pembelian di website Indoteknik. Hubungi kami disini</span> - </Alert> - </div> - - <LineDivider/> - - <div className="p-4"> - <div className="flex justify-between items-center"> - <h2>Alamat Pengiriman</h2> - <Link className="text-caption-1" href="/my/address?select=shipping">Pilih Alamat Lain</Link> - </div> - - { selectedAddress.shipping && ( - <div className="mt-4 text-caption-1"> - <div className="badge-red mb-2">{ selectedAddress.shipping.type.charAt(0).toUpperCase() + selectedAddress.shipping.type.slice(1) + ' Address' }</div> - <p className="font-medium">{ selectedAddress.shipping.name }</p> - <p className="mt-2 text-gray_r-11">{ selectedAddress.shipping.mobile }</p> - <p className="mt-1 text-gray_r-11">{ selectedAddress.shipping.street }, { selectedAddress.shipping?.city?.name }</p> - </div> - ) } - </div> - - <LineDivider/> - - <div className="p-4 flex flex-col gap-y-4"> - {products.map((product, index) => ( - <VariantCard - data={product} - openOnClick={false} - key={index} - /> - ))} - </div> - - <LineDivider/> - - <div className="p-4"> - <div className="flex justify-between items-center"> - <h2>Ringkasan Pesanan</h2> - <p className="text-gray_r-11 text-caption-1">{products.length} Barang</p> - </div> - <hr className="my-4 border-gray_r-6"/> - <div className="flex flex-col gap-y-4"> - <div className="flex gap-x-2 justify-between"> - <p>Total Belanja</p> - <p className="font-medium">{currencyFormat(totalAmount)}</p> - </div> - <div className="flex gap-x-2 justify-between"> - <p>Total Diskon</p> - <p className="font-medium text-red_r-11">- {currencyFormat(totalDiscountAmount)}</p> - </div> - <div className="flex gap-x-2 justify-between"> - <p>Subtotal</p> - <p className="font-medium">{currencyFormat(totalAmount - totalDiscountAmount)}</p> - </div> - <div className="flex gap-x-2 justify-between"> - <p>PPN 11% (Incl.)</p> - <p className="font-medium">{currencyFormat((totalAmount - totalDiscountAmount) * 0.11)}</p> - </div> - </div> - <hr className="my-4 border-gray_r-6"/> - <div className="flex gap-x-2 justify-between mb-4"> - <p>Grand Total</p> - <p className="font-medium text-yellow_r-11">{currencyFormat(totalAmount - totalDiscountAmount)}</p> - </div> - <p className="text-caption-2 text-gray_r-10 mb-2">*) Belum termasuk biaya pengiriman</p> - <p className="text-caption-2 text-gray_r-10 leading-5"> - Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui <Link href="/">Syarat & Ketentuan</Link> yang berlaku - </p> - </div> - - <LineDivider/> - - <div className="p-4"> - <div className="flex justify-between items-center"> - <h2>Alamat Penagihan</h2> - <Link className="text-caption-1" href="/my/address?select=invoicing">Pilih Alamat Lain</Link> - </div> - - { selectedAddress.invoicing && ( - <div className="mt-4 text-caption-1"> - <div className="badge-red mb-2">{ selectedAddress.invoicing.type.charAt(0).toUpperCase() + selectedAddress.invoicing.type.slice(1) + ' Address' }</div> - <p className="font-medium">{ selectedAddress.invoicing.name }</p> - <p className="mt-2 text-gray_r-11">{ selectedAddress.invoicing.mobile }</p> - <p className="mt-1 text-gray_r-11">{ selectedAddress.invoicing.street } { selectedAddress.invoicing.street2 }</p> - </div> - ) } - </div> - - <LineDivider/> - - <div className="p-4"> - <h2>Metode Pembayaran <span className="font-normal text-gray_r-11">(Wajib dipilih)</span></h2> - <div className="grid gap-y-3 mt-4"> - { payments.map((payment, index) => ( - <button - type="button" - className={"text-left border border-gray_r-6 rounded-md p-3 " + (selectedPayment == payment.name && 'border-yellow_r-10 bg-yellow_r-3')} - onClick={() => setSelectedPayment(payment.name)} - key={index} - > - <p>{payment.name} - {payment.number}</p> - <p className="mt-1 text-gray_r-11">PT. Indoteknik Dotcom Gemilang</p> - </button> - )) } - </div> - </div> - - <LineDivider/> - - <div className="p-4"> - <h2>Purchase Order</h2> - - <div className="mt-4 flex gap-x-3"> - <div className="w-6/12"> - <label className="form-label font-normal"> - Dokumen PO - </label> - <input - type="file" - className="form-input mt-2 h-12" - accept="image/*,application/pdf" - onChange={(e) => setPoFile(e.target.files[0])} - /> - </div> - <div className="w-6/12"> - <label className="form-label font-normal">Nomor PO</label> - <input - type="text" - className="form-input mt-2 h-12" - value={poNumber} - onChange={(e) => setPoNumber(e.target.value)} - /> - </div> - </div> - <p className="text-caption-2 text-gray_r-11 mt-2">Ukuran dokumen PO Maksimal 5MB</p> - </div> - - <LineDivider/> - - <div className="flex gap-x-3 p-4"> - <button - className="flex-1 btn-yellow" - onClick={checkout} - disabled={isLoading} - > - { isLoading && 'Loading...' } - { !isLoading && 'Bayar' } - </button> - </div> - </> - ) } - </Layout> - </WithAuth> - ) -}
\ No newline at end of file diff --git a/src/pages/shop/product/[slug].js b/src/pages/shop/product/[slug].js deleted file mode 100644 index 61692c1c..00000000 --- a/src/pages/shop/product/[slug].js +++ /dev/null @@ -1,305 +0,0 @@ -import Link from "@/components/elements/Link" -import { useRouter } from "next/router" -import { useEffect, useState } from "react" -import Header from "@/components/layouts/Header" -import apiOdoo from "@/core/utils/apiOdoo" -import { createSlug, getIdFromSlug } from "@/core/utils/slug" -import currencyFormat from "@/core/utils/currencyFormat" -import Layout from "@/components/layouts/Layout" -import { createOrUpdateItemCart } from "@/core/utils/cart" -import toast from "react-hot-toast" -import Footer from "@/components/layouts/Footer" -import Image from "@/components/elements/Image" -import LineDivider from "@/components/elements/LineDivider" -import { HeartIcon as HeartIconSolid } from "@heroicons/react/24/solid" -import { useAuth } from "@/core/utils/auth" -import { HeartIcon } from "@heroicons/react/24/outline" -import LazyLoad from "react-lazy-load" -import ProductSimilar from "@/components/products/ProductSimilar" - -export async function getServerSideProps( context ) { - const { slug } = context.query - let product = await apiOdoo('GET', '/api/v1/product/' + getIdFromSlug(slug)) - if (product?.length == 1) { - product = product[0] - product.description = product.description.replaceAll('<p>', '||p||') - product.description = product.description.replaceAll('</p>', '||/p||') - product.description = product.description.replace(/(<([^>]+)>)/gi, ' ') - product.description = product.description.replaceAll('||p||', '<p>') - product.description = product.description.replaceAll('||/p||', '</p>') - product.description = product.description.trim() - } - return { props: { product } } -} - -export default function ProductDetail({ product }) { - const [ auth ] = useAuth() - const router = useRouter() - const { slug } = router.query - const [selectedVariant, setSelectedVariant] = useState("") - const [quantity, setQuantity] = useState("1") - const [activeVariant, setActiveVariant] = useState({ - id: product.id, - code: product.code, - price: product.lowest_price, - stock: product.stock_total, - weight: product.weight, - attributes: '', - }) - - const [ isAddedToWishlist, setAddedToWishlist ] = useState(false) - const [ activeTab, setActiveTab ] = useState('specification') - - const addOrDeleteWishlist = async () => { - if (auth) { - await apiOdoo('POST', `/api/v1/user/${auth.id}/wishlist/create-or-delete`, { - product_id: product.id - }) - if (isAddedToWishlist) { - toast.success('Berhasil menghapus dari wishlist') - } else { - toast.success('Berhasil menambahkan ke wishlist') - } - setAddedToWishlist(!isAddedToWishlist) - } else { - toast.error('Login terlebih dahulu untuk melanjutkan') - router.push('/login') - } - } - - useEffect(() => { - if (auth) { - const checkWishlist = async () => { - const wishlist = await apiOdoo('GET', `/api/v1/user/${auth.id}/wishlist?product_id=${product.id}`) - setAddedToWishlist(wishlist.product_total > 0 ? true : false) - } - checkWishlist() - } - }, [ auth, product ]) - - useEffect(() => { - if (product.variants.length == 1) { - setSelectedVariant(product.variants[0].id) - } - }, [ product ]) - - useEffect(() => { - if (selectedVariant != '') { - let newActiveVariant = product.variants.filter((variant) => { - return variant.id == selectedVariant - }) - - if (newActiveVariant.length == 1) { - newActiveVariant = newActiveVariant[0] - setActiveVariant({ - id: newActiveVariant.id, - code: newActiveVariant.code, - price: newActiveVariant.price, - stock: newActiveVariant.stock, - weight: newActiveVariant.weight, - attributes: newActiveVariant.attributes.join(', '), - }) - } - } - }, [selectedVariant, product]) - - const onchangeVariant = (e) => { - setSelectedVariant(e.target.value) - } - - const onChangeQuantity = (e) => { - let inputValue = e.target.value - inputValue = parseInt(inputValue) - inputValue = Math.floor(inputValue) - setQuantity(inputValue) - } - - const addItemToCart = () => { - if (product.variant_total > 1 && !selectedVariant) { - toast.error('Pilih varian terlebih dahulu untuk menambahkan ke keranjang', { duration: 2000 }) - return false - } - - if (quantity > 0) { - toast.success('Berhasil menambahkan ke keranjang', { duration: 1500 }) - createOrUpdateItemCart(activeVariant.id, parseInt(quantity)) - } else { - toast.error('Jumlah barang yang ditambahkan minimal 1 pcs', { duration: 2000 }) - } - - return true - } - - const checkoutProduct = () => { - if (!auth) { - toast.error('Login terlebih dahulu untuk melanjutkan', { duration: 2000 }) - router.push('/login') - return - } - if (product.variant_total > 1 && !selectedVariant) { - toast.error('Pilih varian terlebih dahulu untuk melanjutkan pembelian', { duration: 2000 }) - return - } - if (quantity < 0) { - toast.error('Jumlah barang yang ditambahkan minimal 1 pcs', { duration: 2000 }) - return - } - router.push(`/shop/checkout?product_id=${activeVariant.id}&qty=${quantity}`) - } - - const TabButton = ({ children, name }) => ( - <button - type="button" - className={`font-medium pb-1 ${activeTab == name ? 'text-red_r-11 border-b border-red_r-10' : 'text-gray_r-11'}`} - onClick={() => setActiveTab(name)} - > - { children } - </button> - ) - - return ( - <> - <Header title={`${product.name} - Indoteknik`}/> - <Layout> - <Image - src={product.image} - alt={product.name} - className="border-b border-gray_r-6 w-full h-[300px] object-contain object-center bg-white" - /> - - <div className="p-4"> - <div className="flex justify-between gap-x-3"> - <div> - <Link href={'/shop/brands/' + createSlug(product.manufacture.name, product.manufacture.id)}> - {product.manufacture.name ?? '-'} - </Link> - <h1 className="h2 mt-2 mb-3">{product.name}{activeVariant.attributes ? ' - ' + activeVariant.attributes : ''}</h1> - </div> - <button className="h-fit" onClick={addOrDeleteWishlist}> - { isAddedToWishlist && ( - <HeartIconSolid className="w-6 text-red_r-10" /> - ) } - { !isAddedToWishlist && ( - <HeartIcon className="w-6" /> - ) } - </button> - </div> - - {product.variant_total > 1 && !selectedVariant && product.lowest_price.price > 0 ? ( - <p className="text-caption-2 text-gray-800 mb-1">Harga mulai dari:</p> - ) : ''} - - {product.lowest_price.discount_percentage > 0 ? ( - <div className="flex gap-x-1 items-center mb-1"> - <p className="text-caption-2 text-gray_r-11 line-through">{currencyFormat(activeVariant.price.price)}</p> - <span className="badge-solid-red">{activeVariant.price.discount_percentage}%</span> - </div> - ) : ''} - - {product.lowest_price.price > 0 ? ( - <p className="text-body-lg font-semibold">{currencyFormat(activeVariant.price.price_discount)}</p> - ) : ( - <p className="text-gray_r-11">Dapatkan harga terbaik, <a href="">hubungi kami.</a></p> - )} - </div> - - <LineDivider /> - - <div className="p-4"> - <div className=""> - <label className="form-label mb-2">Pilih: <span className="text-gray_r-11 font-normal">{product.variant_total} Varian</span></label> - <select name="variant" className="form-input" value={selectedVariant} onChange={onchangeVariant} > - <option value="" disabled={selectedVariant != "" ? true : false}>Pilih Varian...</option> - {product.variants.length > 1 ? ( - product.variants.map((variant) => { - return ( - <option key={variant.id} value={variant.id}>{variant.attributes.join(', ')}</option> - ) - }) - ) : ( - <option key={product.variants[0].id} value={product.variants[0].id}>{product.variants[0].name}</option> - )} - </select> - </div> - - <label htmlFor="quantity" className="form-label mb-1 mt-3">Jumlah</label> - <div className="flex gap-x-2 mt-2"> - <input type="number" name="quantity" id="quantity" className="form-input h-full w-5/12 text-center" value={quantity} onChange={onChangeQuantity} /> - - <button - className="btn-yellow w-full" - onClick={addItemToCart} - disabled={(product.lowest_price.price == 0 ? true : false)} - > - Keranjang - </button> - <button - onClick={checkoutProduct} - className="btn-solid-red w-full" - > - Beli - </button> - </div> - </div> - - <LineDivider /> - - <div className="p-4"> - <h2 className="font-bold mb-4">Informasi Produk</h2> - <div className="flex gap-x-3 mb-4"> - <TabButton name="specification">Spesifikasi</TabButton> - <TabButton name="description">Deskripsi</TabButton> - <TabButton name="information">Info Penting</TabButton> - </div> - - <div className={`border border-gray_r-6 rounded divide-y ${activeTab == 'specification' ? 'block' : 'hidden'}`}> - <ProductSpecification label="Jumlah Varian"> - <p className="text-gray-800">{product.variant_total} Varian</p> - </ProductSpecification> - <ProductSpecification label="Nomor SKU"> - <p className="text-gray-800" id="sku_number">SKU-{activeVariant.id}</p> - </ProductSpecification> - <ProductSpecification label="Part Number"> - <p className="text-gray-800" id="part_number">{activeVariant.code}</p> - </ProductSpecification> - <ProductSpecification label="Stok"> - <div className="flex gap-x-2" id="stock"> - {activeVariant.stock > 0 ? (activeVariant.stock > 5 && ( - <> - <div className="badge-solid-red">Ready Stock</div> - <div className="badge-gray">{activeVariant.stock > 5 ? '> 5' : '< 5'}</div> - </> - )) : '0'} - </div> - </ProductSpecification> - <ProductSpecification label="Part Number"> - <p className="text-gray-800" id="weight">{activeVariant.weight > 0 ? activeVariant.weight : '1'} KG</p> - </ProductSpecification> - </div> - - <div - className={`text-gray-800 leading-7 ${activeTab == 'description' ? 'block' : 'hidden'}`} - dangerouslySetInnerHTML={{__html: (product.description != '' ? product.description : 'Belum ada deskripsi produk.')}} - ></div> - </div> - - <LineDivider /> - - <LazyLoad> - <ProductSimilar productId={getIdFromSlug(slug || '')} /> - </LazyLoad> - - <Footer /> - </Layout> - </> - ) -} - -const ProductSpecification = ({ children, ...props }) => { - return ( - <div className="flex p-3 justify-between items-center gap-x-1"> - <h3 className="text-gray-900">{ props.label }</h3> - { children } - </div> - ) -}
\ No newline at end of file diff --git a/src/pages/shop/product/[slug].jsx b/src/pages/shop/product/[slug].jsx new file mode 100644 index 00000000..84a3c7d4 --- /dev/null +++ b/src/pages/shop/product/[slug].jsx @@ -0,0 +1,29 @@ +import Seo from "@/core/components/Seo" +import BasicLayout from "@/core/components/layouts/BasicLayout" +import { getIdFromSlug } from "@/core/utils/slug" +import productApi from "@/lib/product/api/productApi" +import Product from "@/lib/product/components/Product" + +export async function getServerSideProps(context) { + const { slug } = context.query + let product = await productApi({ id: getIdFromSlug(slug) }) + if (product?.length == 1) { + product = product[0] + product.description = product.description.replaceAll('<p>', '||p||') + product.description = product.description.replaceAll('</p>', '||/p||') + product.description = product.description.replace(/(<([^>]+)>)/gi, ' ') + product.description = product.description.replaceAll('||p||', '<p>') + product.description = product.description.replaceAll('||/p||', '</p>') + product.description = product.description.trim() + } + return { props: { product } } +} + +export default function ProductDetail({ product }) { + return ( + <BasicLayout> + <Seo title={product?.name} /> + <Product product={product} /> + </BasicLayout> + ) +}
\ No newline at end of file diff --git a/src/pages/shop/quotation/finish.js b/src/pages/shop/quotation/finish.js deleted file mode 100644 index f7983fef..00000000 --- a/src/pages/shop/quotation/finish.js +++ /dev/null @@ -1,39 +0,0 @@ -import WithAuth from "@/components/auth/WithAuth"; -import Link from "@/components/elements/Link"; -import Header from "@/components/layouts/Header"; -import Layout from "@/components/layouts/Layout"; -import { useAuth } from "@/core/utils/auth"; -import { EnvelopeIcon } from "@heroicons/react/24/outline"; -import { useRouter } from "next/router"; - -export default function FinishQuotation() { - const router = useRouter(); - const { id } = router.query; - const [ auth ] = useAuth(); - - return ( - <WithAuth> - <Layout> - <Header title="Penawaran Harga" /> - - <div className="m-4 px-4 py-6 shadow-md border border-gray_r-3"> - <div className="flex"> - <span className="p-3 mx-auto bg-yellow_r-3 border border-yellow_r-6 rounded"> - <EnvelopeIcon className="w-8 text-yellow_r-11" /> - </span> - </div> - <p className="h2 text-center mt-6"> - Terima Kasih { auth?.name } - </p> - <p className="text-center mt-3 leading-6 text-gray_r-11"> - Penawaran harga kamu di Indoteknik.com berhasil dikirimkan, tim kami akan segera menghubungi anda. - </p> - { id && ( - <Link href={`/my/transaction/${id}`} className="btn-yellow text-gray_r-12 mt-6 w-full">Lihat Penawaran</Link> - )} - <Link href="/" className="btn-light text-gray_r-12 mt-2 w-full">Ke Halaman Utama</Link> - </div> - </Layout> - </WithAuth> - ); -}
\ No newline at end of file diff --git a/src/pages/shop/quotation/index.js b/src/pages/shop/quotation/index.js deleted file mode 100644 index e1c196db..00000000 --- a/src/pages/shop/quotation/index.js +++ /dev/null @@ -1,140 +0,0 @@ -import WithAuth from "@/components/auth/WithAuth"; -import LineDivider from "@/components/elements/LineDivider"; -import Link from "@/components/elements/Link"; -import AppBar from "@/components/layouts/AppBar"; -import Layout from "@/components/layouts/Layout"; -import VariantCard from "@/components/variants/VariantCard"; -import apiOdoo from "@/core/utils/apiOdoo"; -import { useAuth } from "@/core/utils/auth"; -import { deleteItemCart, getCart } from "@/core/utils/cart"; -import currencyFormat from "@/core/utils/currencyFormat"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; -import { toast } from "react-hot-toast"; - -export default function Quotation() { - const router = useRouter(); - const [ auth ] = useAuth(); - const [ products, setProducts ] = useState([]); - const [ totalAmount, setTotalAmount ] = useState(0); - const [ totalDiscountAmount, setTotalDiscountAmount ] = useState(0); - - useEffect(() => { - const getProducts = async () => { - let cart = getCart(); - let productIds = Object - .values(cart) - .filter((itemCart) => itemCart.selected == true) - .map((itemCart) => itemCart.product_id); - if (productIds.length > 0) { - productIds = productIds.join(','); - let dataProducts = await apiOdoo('GET', `/api/v1/product_variant/${productIds}`); - dataProducts = dataProducts.map((product) => ({ - ...product, - quantity: cart[product.id].quantity, - selected: cart[product.id].selected, - })); - setProducts(dataProducts); - } - }; - getProducts(); - }, [ router, auth ]); - - useEffect(() => { - if (products) { - let calculateTotalAmount = 0; - let calculateTotalDiscountAmount = 0; - products.forEach(product => { - calculateTotalAmount += product.price.price * product.quantity; - calculateTotalDiscountAmount += (product.price.price - product.price.price_discount) * product.quantity; - }); - setTotalAmount(calculateTotalAmount); - setTotalDiscountAmount(calculateTotalDiscountAmount); - } - }, [products]); - - const submitQuotation = async () => { - let productOrder = products.map((product) => ({ 'product_id': product.id, 'quantity': product.quantity })); - let data = { - 'partner_shipping_id': auth.partner_id, - 'partner_invoice_id': auth.partner_id, - 'order_line': JSON.stringify(productOrder) - }; - const quotation = await apiOdoo('POST', `/api/v1/partner/${auth.partner_id}/sale_order/checkout`, data); - for (const product of products) { - deleteItemCart(product.id); - } - if (quotation?.id) { - router.push(`/shop/quotation/finish?id=${quotation.id}`); - return; - }; - toast.error('Terdapat kesalahan internal, hubungi kami'); - } - return ( - <WithAuth> - <Layout> - <AppBar title="Penawaran Harga" /> - - <div className="p-4 flex flex-col gap-y-4"> - <p className="h2">Produk</p> - {products.map((product, index) => ( - <VariantCard - data={product} - openOnClick={false} - key={index} - /> - ))} - </div> - - <LineDivider /> - - <div className="p-4"> - <div className="flex justify-between items-center"> - <p className="h2">Ringkasan Penawaran</p> - <p className="text-gray_r-11 text-caption-1">{products.length} Barang</p> - </div> - <hr className="my-4 border-gray_r-6"/> - <div className="flex flex-col gap-y-4"> - <div className="flex gap-x-2 justify-between"> - <p>Total Belanja</p> - <p className="font-medium">{currencyFormat(totalAmount)}</p> - </div> - <div className="flex gap-x-2 justify-between"> - <p>Total Diskon</p> - <p className="font-medium text-red_r-11">{'- ' + currencyFormat(totalDiscountAmount)}</p> - </div> - <div className="flex gap-x-2 justify-between"> - <p>Subtotal</p> - <p className="font-medium">{currencyFormat(totalAmount - totalDiscountAmount)}</p> - </div> - <div className="flex gap-x-2 justify-between"> - <p>PPN 11% (Incl.)</p> - <p className="font-medium">{currencyFormat((totalAmount - totalDiscountAmount) * 0.11)}</p> - </div> - </div> - <hr className="my-4 border-gray_r-6"/> - <div className="flex gap-x-2 justify-between mb-4"> - <p>Grand Total</p> - <p className="font-medium text-yellow_r-11">{currencyFormat(totalAmount - totalDiscountAmount)}</p> - </div> - <p className="text-caption-2 text-gray_r-10 mb-2">*) Belum termasuk biaya pengiriman</p> - <p className="text-caption-2 text-gray_r-10 leading-5"> - Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui <Link href="/">Syarat & Ketentuan</Link> yang berlaku - </p> - </div> - - <LineDivider /> - - <div className="p-4"> - <button - type="button" - className="btn-yellow w-full" - onClick={submitQuotation} - > - Kirim Penawaran - </button> - </div> - </Layout> - </WithAuth> - ) -}
\ No newline at end of file diff --git a/src/pages/shop/search.js b/src/pages/shop/search.js deleted file mode 100644 index 4152bd43..00000000 --- a/src/pages/shop/search.js +++ /dev/null @@ -1,125 +0,0 @@ -import axios from "axios"; -import Header from "@/components/layouts/Header"; -import Layout from "@/components/layouts/Layout"; -import Pagination from "@/components/elements/Pagination"; -import ProductCard from "@/components/products/ProductCard"; -import FilterIcon from "@/icons/filter.svg"; -import { useEffect, useState } from "react"; -import Filter from "@/components/elements/Filter"; -import Footer from "@/components/layouts/Footer"; - -export async function getServerSideProps(context) { - const { - q = '*', - page = 1, - brand = '', - category = '', - price_from = '', - price_to = '', - order_by = '', - } = context.query; - - let urlParameter = [ - `page=${page}`, - `brand=${brand}`, - `category=${category}`, - `price_from=${price_from}`, - `price_to=${price_to}`, - `order_by=${order_by}` - ].join('&'); - let searchResults = await axios(`${process.env.SELF_HOST}/api/shop/search?q=${q}&${urlParameter}`); - searchResults = searchResults.data; - return { props: { searchResults, q, page, brand, category, price_from, price_to, order_by } }; -} - -export default function ShopSearch({ - searchResults, - q, - page, - brand, - category, - price_from, - price_to, - order_by -}) { - const pageCount = Math.ceil(searchResults.response.numFound / searchResults.responseHeader.params.rows); - const productStart = searchResults.responseHeader.params.start; - const productRows = searchResults.responseHeader.params.rows; - const productFound = searchResults.response.numFound; - - // Variable for <Filter/> props state - const [activeFilter, setActiveFilter] = useState(false); - const [filterCount, setFilterCount] = useState(0); - - const route = () => { - let route = `/shop/search?q=${q}`; - if (brand) route += `&brand=${brand}`; - if (category) route += `&category=${category}`; - if (price_from) route += `&price_from=${price_from}`; - if (price_to) route += `&price_to=${price_to}`; - if (order_by) route += `&order_by=${order_by}`; - return route; - } - - useEffect(() => { - let calculateFilterCount = 0; - if (brand) calculateFilterCount++; - if (category) calculateFilterCount++; - if (price_from || price_to) calculateFilterCount++; - if (order_by) calculateFilterCount++; - setFilterCount(calculateFilterCount); - }, [brand, category, price_from, price_to, order_by]); - - return ( - <> - <Header title={`Jual ${q} - Indoteknik`} /> - <Filter - defaultRoute={`/shop/search?q=${q}`} - isActive={activeFilter} - closeFilter={() => setActiveFilter(false)} - defaultPriceFrom={price_from} - defaultPriceTo={price_to} - defaultBrand={brand} - defaultCategory={category} - defaultOrderBy={order_by} - searchResults={searchResults} - /> - <Layout> - <div className="p-4"> - <h1 className="mb-2">Produk</h1> - <div className="text-caption-1 mb-4"> - {productFound > 0 ? ( - <> - Menampilkan - {pageCount > 1 ? ( - <> - {productStart + 1}-{ - (productStart + productRows) > productFound ? productFound : productStart + productRows - } - dari - </> - ) : ''} - {searchResults.response.numFound} - produk { q != '*' && (<>untuk pencarian <span className="font-semibold">{q}</span></>) } - </> - ) : 'Mungkin yang anda cari'} - </div> - <button className="btn-light py-2 flex items-center gap-x-2 mb-4" onClick={() => setActiveFilter(true)}> - <FilterIcon className="w-4 h-4" /> <span>Filter {filterCount > 0 ? `(${filterCount})` : ''}</span> - </button> - <div className="grid grid-cols-2 gap-3"> - {searchResults.response.products.map((product) => ( - <ProductCard key={product.id} data={product} /> - ))} - </div> - - <div className="mt-4"> - <Pagination pageCount={pageCount} currentPage={parseInt(page)} url={route()} /> - </div> - </div> - - <Footer /> - </Layout> - </> - ) -}
\ No newline at end of file diff --git a/src/pages/shop/search.jsx b/src/pages/shop/search.jsx new file mode 100644 index 00000000..e86b1f4e --- /dev/null +++ b/src/pages/shop/search.jsx @@ -0,0 +1,19 @@ +import BasicLayout from "@/core/components/layouts/BasicLayout" +import ProductSearch from "@/lib/product/components/ProductSearch" +import { useRouter } from "next/router" +import _ from "lodash-contrib" + +export default function Search() { + const router = useRouter() + + return ( + <BasicLayout> + { !_.isEmpty(router.query) && ( + <ProductSearch + query={router.query} + prefixUrl="/shop/search" + /> + ) } + </BasicLayout> + ) +}
\ No newline at end of file |
