diff options
Diffstat (limited to 'src2/pages/shop')
| -rw-r--r-- | src2/pages/shop/brands/[slug].js | 178 | ||||
| -rw-r--r-- | src2/pages/shop/brands/index.js | 79 | ||||
| -rw-r--r-- | src2/pages/shop/cart.js | 282 | ||||
| -rw-r--r-- | src2/pages/shop/checkout/finish.js | 47 | ||||
| -rw-r--r-- | src2/pages/shop/checkout/index.js | 325 | ||||
| -rw-r--r-- | src2/pages/shop/product/[slug].js | 305 | ||||
| -rw-r--r-- | src2/pages/shop/quotation/finish.js | 39 | ||||
| -rw-r--r-- | src2/pages/shop/quotation/index.js | 140 | ||||
| -rw-r--r-- | src2/pages/shop/search.js | 125 |
9 files changed, 1520 insertions, 0 deletions
diff --git a/src2/pages/shop/brands/[slug].js b/src2/pages/shop/brands/[slug].js new file mode 100644 index 00000000..a387e55d --- /dev/null +++ b/src2/pages/shop/brands/[slug].js @@ -0,0 +1,178 @@ +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/src2/pages/shop/brands/index.js b/src2/pages/shop/brands/index.js new file mode 100644 index 00000000..bfdcd403 --- /dev/null +++ b/src2/pages/shop/brands/index.js @@ -0,0 +1,79 @@ +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/src2/pages/shop/cart.js b/src2/pages/shop/cart.js new file mode 100644 index 00000000..1178781b --- /dev/null +++ b/src2/pages/shop/cart.js @@ -0,0 +1,282 @@ +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/src2/pages/shop/checkout/finish.js b/src2/pages/shop/checkout/finish.js new file mode 100644 index 00000000..df284f8a --- /dev/null +++ b/src2/pages/shop/checkout/finish.js @@ -0,0 +1,47 @@ +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/src2/pages/shop/checkout/index.js b/src2/pages/shop/checkout/index.js new file mode 100644 index 00000000..0a77ebed --- /dev/null +++ b/src2/pages/shop/checkout/index.js @@ -0,0 +1,325 @@ +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/src2/pages/shop/product/[slug].js b/src2/pages/shop/product/[slug].js new file mode 100644 index 00000000..61692c1c --- /dev/null +++ b/src2/pages/shop/product/[slug].js @@ -0,0 +1,305 @@ +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/src2/pages/shop/quotation/finish.js b/src2/pages/shop/quotation/finish.js new file mode 100644 index 00000000..f7983fef --- /dev/null +++ b/src2/pages/shop/quotation/finish.js @@ -0,0 +1,39 @@ +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/src2/pages/shop/quotation/index.js b/src2/pages/shop/quotation/index.js new file mode 100644 index 00000000..e1c196db --- /dev/null +++ b/src2/pages/shop/quotation/index.js @@ -0,0 +1,140 @@ +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/src2/pages/shop/search.js b/src2/pages/shop/search.js new file mode 100644 index 00000000..4152bd43 --- /dev/null +++ b/src2/pages/shop/search.js @@ -0,0 +1,125 @@ +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 |
