diff options
| author | trisusilo48 <tri.susilo@altama.co.id> | 2024-07-10 15:58:51 +0700 |
|---|---|---|
| committer | trisusilo48 <tri.susilo@altama.co.id> | 2024-07-10 15:58:51 +0700 |
| commit | 2e3c726bc8217f3960cfecec44b81303b03de72b (patch) | |
| tree | 1b85ced7f61f3e4c3f1f27b577b37aa161615065 /src-migrate/pages | |
| parent | 2b3bd9c0a454dbad69ce29cee877bfb1fca5dfa6 (diff) | |
| parent | a99bf6480eea556e53b85e6db45f3b8c2361e693 (diff) | |
Merge branch 'release' into development
# Conflicts:
# src/pages/shop/product/variant/[slug].jsx
Diffstat (limited to 'src-migrate/pages')
| -rw-r--r-- | src-migrate/pages/_app.tsx | 4 | ||||
| -rw-r--r-- | src-migrate/pages/api/product-variant/[id].tsx | 53 | ||||
| -rw-r--r-- | src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx | 49 | ||||
| -rw-r--r-- | src-migrate/pages/api/product-variant/[id]/promotion/highlight.tsx | 58 | ||||
| -rw-r--r-- | src-migrate/pages/api/promotion-program/[id].tsx | 42 | ||||
| -rw-r--r-- | src-migrate/pages/register.tsx | 2 | ||||
| -rw-r--r-- | src-migrate/pages/shop/cart/cart.module.css | 35 | ||||
| -rw-r--r-- | src-migrate/pages/shop/cart/index.tsx | 156 | ||||
| -rw-r--r-- | src-migrate/pages/shop/product/[slug].tsx | 83 |
9 files changed, 479 insertions, 3 deletions
diff --git a/src-migrate/pages/_app.tsx b/src-migrate/pages/_app.tsx index 2dc82559..36640c04 100644 --- a/src-migrate/pages/_app.tsx +++ b/src-migrate/pages/_app.tsx @@ -1,5 +1,5 @@ -import '~/common/styles/fonts/Inter/inter.css' -import '~/common/styles/globals.css' +import '~/styles/fonts/Inter/inter.css' +import '~/styles/globals.css' import type { AppProps } from "next/app" export default function MyApp({ Component, pageProps }: AppProps) { diff --git a/src-migrate/pages/api/product-variant/[id].tsx b/src-migrate/pages/api/product-variant/[id].tsx new file mode 100644 index 00000000..955fde6a --- /dev/null +++ b/src-migrate/pages/api/product-variant/[id].tsx @@ -0,0 +1,53 @@ +import moment from "moment"; +import { NextApiRequest, NextApiResponse } from "next"; +import { SolrResponse } from "~/types/solr"; + +const SOLR_HOST = process.env.SOLR_HOST as string + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const variantId = req.query.id as string + let price_tier = 'tier1' + + let auth = req.cookies.auth ? JSON.parse(req.cookies.auth) : null + if (auth?.pricelist) price_tier = auth.pricelist + + if (req.method === 'GET') { + const queryParams = new URLSearchParams({ q: `id:${variantId}` }) + const response = await fetch(`${SOLR_HOST}/solr/variants/select?${queryParams.toString()}`) + const data: SolrResponse<any[]> = await response.json() + + if (data.response.numFound === 0) { + res.status(404).json({ error: 'Variant not found' }) + return + } + + const variant = await map(data.response.docs[0], price_tier) + + res.status(200).json({ data: variant }) + } +} + +const map = async (variant: any, price_tier: string) => { + const data: any = {} + const price = variant[`price_${price_tier}_v2_f`] || 0 + + data.id = parseInt(variant.id) + data.parent_id = variant.template_id_i + data.display_name = variant.display_name_s + data.image = variant.image_s + data.name = variant.name_s + data.default_code = variant.default_code_s + data.price = { discount_percentage: 0, price, price_discount: price } + + return data +} + +const checkIsFlashsale = (variant: any) => { + const endDateStr = variant.flashsale_end_date_s || null + if (!endDateStr) return false + + const now = moment() + const endDate = moment(endDateStr, 'YYYY-MM-DD HH:mm:ss') + + return variant.flashsale_id_i > 0 && now.isSameOrBefore(endDate) +}
\ No newline at end of file diff --git a/src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx b/src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx new file mode 100644 index 00000000..8da0d9a3 --- /dev/null +++ b/src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx @@ -0,0 +1,49 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { SolrResponse } from "~/types/solr"; + +const SOLR_HOST = process.env.SOLR_HOST as string + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const productId = req.query.id as string + const category = req.query.category as string + + if (req.method === 'GET') { + const queryParams = new URLSearchParams({ q: `product_ids:${productId}` }) + queryParams.append('fq', `type_value_s:${category}`) + queryParams.append('fq', `active_b:true`) + + const response = await fetch(`${SOLR_HOST}/solr/promotion_program_lines/select?${queryParams.toString()}`) + const data: SolrResponse<any[]> = await response.json() + + const promotions = await map(data.response.docs) + res.status(200).json({ data: promotions }) + } +} + +const map = async (promotions: any[]) => { + const result = [] + + for (const promotion of promotions) { + const data: any = {} + + data.id = promotion.id + data.program_id = promotion.program_id_i + data.name = promotion.name_s + data.type = { + value: promotion.type_value_s, + label: promotion.type_label_s, + } + data.limit = promotion.package_limit_i + data.limit_user = promotion.package_limit_user_i + data.limit_trx = promotion.package_limit_trx_i + data.price = promotion.price_f + data.total_qty = promotion.total_qty_i + + data.products = JSON.parse(promotion.products_s) + data.free_products = JSON.parse(promotion.free_products_s) + + result.push(data) + } + + return result +}
\ No newline at end of file diff --git a/src-migrate/pages/api/product-variant/[id]/promotion/highlight.tsx b/src-migrate/pages/api/product-variant/[id]/promotion/highlight.tsx new file mode 100644 index 00000000..c4acacf1 --- /dev/null +++ b/src-migrate/pages/api/product-variant/[id]/promotion/highlight.tsx @@ -0,0 +1,58 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { SolrResponse } from "~/types/solr"; + +const SOLR_HOST = process.env.SOLR_HOST as string + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const productId = req.query.id as string + + if (req.method === 'GET') { + const types = ['bundling', 'discount_loading', 'merchandise'] + const queryParams = new URLSearchParams({ + q: `product_ids:${productId}`, + rows: '1' + }) + + let programs: any[] = [] + + for (const type of types) { + queryParams.set('fq', `type_value_s:${type}`) + queryParams.append('fq', `active_b:true`) + + const response = await fetch(`${SOLR_HOST}/solr/promotion_program_lines/select?${queryParams.toString()}`) + const data: SolrResponse<any[]> = await response.json() + programs.push(...data.response.docs) + } + + programs = await extractPrograms(programs) + res.status(200).json({ data: programs }) + } +} + +const extractPrograms = async (programs: any[]) => { + const result = [] + + for (const program of programs) { + const data: any = {} + + data.id = program.id + data.program_id = program.program_id_i + data.name = program.name_s + data.type = { + value: program.type_value_s, + label: program.type_label_s, + } + data.limit = program.package_limit_i + data.limit_user = program.package_limit_user_i + data.limit_trx = program.package_limit_trx_i + data.price = program.price_f + data.total_qty = program.total_qty_i + + data.products = JSON.parse(program.products_s) + data.free_products = JSON.parse(program.free_products_s) + + result.push(data) + } + + return result +}
\ No newline at end of file diff --git a/src-migrate/pages/api/promotion-program/[id].tsx b/src-migrate/pages/api/promotion-program/[id].tsx new file mode 100644 index 00000000..c509b802 --- /dev/null +++ b/src-migrate/pages/api/promotion-program/[id].tsx @@ -0,0 +1,42 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { SolrResponse } from "~/types/solr"; + +const SOLR_HOST = process.env.SOLR_HOST as string + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const id = req.query.id as string + + if (req.method === 'GET') { + const queryParams = new URLSearchParams({ q: `id:${id}` }) + const response = await fetch(`${SOLR_HOST}/solr/promotion_programs/select?${queryParams.toString()}`) + const data: SolrResponse<any[]> = await response.json() + + if (data.response.numFound === 0) { + res.status(404).json({ error: 'Program not found' }) + return + } + + const program = await map(data.response.docs[0]) + + res.status(200).json({ data: program }) + } +} + +const map = async (program: any) => { + const data: any = {} + + data.id = program.id + data.name = program.name_s + data.start_time = program.start_time_s + data.end_time = program.end_time_s + data.applies_to = program.applies_to_s + data.time_left = (new Date(data.end_time).getTime() - new Date().getTime()) / 1000 + + // const duration = moment.duration(data.time_left, 'seconds') + // const days = duration.days() + // const hours = duration.hours() + // const minutes = duration.minutes() + // const seconds = duration.seconds() + + return data +}
\ No newline at end of file diff --git a/src-migrate/pages/register.tsx b/src-migrate/pages/register.tsx index 1246c6f5..136eaa3b 100644 --- a/src-migrate/pages/register.tsx +++ b/src-migrate/pages/register.tsx @@ -1,7 +1,7 @@ import BasicLayout from "@/core/components/layouts/BasicLayout" import { useWindowSize } from "usehooks-ts" -import Seo from "~/common/components/elements/Seo" +import { Seo } from "~/components/seo" import Register from "~/modules/register" const RegisterPage = () => { diff --git a/src-migrate/pages/shop/cart/cart.module.css b/src-migrate/pages/shop/cart/cart.module.css new file mode 100644 index 00000000..806104be --- /dev/null +++ b/src-migrate/pages/shop/cart/cart.module.css @@ -0,0 +1,35 @@ +.title { + @apply text-h-lg font-semibold; +} + +.content { + @apply flex flex-wrap ; +} + +.item-wrapper { + @apply w-full md:w-3/4 min-h-screen; +} + +.item-skeleton { + @apply grid grid-cols-1 gap-y-4; +} + +.items { + @apply flex flex-col gap-y-6 border-t border-gray-300 pt-6; +} + +.summary-wrapper { + @apply w-full md:w-1/4 md:pl-6 mt-6 md:mt-0 bottom-0 md:sticky sticky bg-white; +} + +.summary { + @apply border border-gray-300 p-4 rounded-md sticky top-[180px]; +} + +.summary-buttons { + @apply grid grid-cols-2 gap-x-3 mt-6; +} + +.summary-buttons-step-approval { + @apply grid grid-cols-1 gap-y-3 mt-6; +} diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx new file mode 100644 index 00000000..d89707d2 --- /dev/null +++ b/src-migrate/pages/shop/cart/index.tsx @@ -0,0 +1,156 @@ +import style from './cart.module.css'; + +import React, { useEffect, useMemo } from 'react'; +import Link from 'next/link'; +import { Button, Tooltip } from '@chakra-ui/react'; + +import { getAuth } from '~/libs/auth'; +import { useCartStore } from '~/modules/cart/stores/useCartStore'; + +import CartItem from '~/modules/cart/components/Item'; +import CartSummary from '~/modules/cart/components/Summary'; +import clsxm from '~/libs/clsxm'; +import useDevice from '@/core/hooks/useDevice'; +import CartSummaryMobile from '~/modules/cart/components/CartSummaryMobile'; +import Image from '~/components/ui/image'; + +const CartPage = () => { + const auth = getAuth(); + const [isStepApproval, setIsStepApproval] = React.useState(false); + + const { loadCart, cart, summary } = useCartStore(); + + const useDivvice = useDevice(); + + useEffect(() => { + if (typeof auth === 'object' && !cart) { + loadCart(auth.id); + setIsStepApproval(auth?.feature?.soApproval); + } + }, [auth, loadCart, cart]); + + const hasSelectedPromo = useMemo(() => { + if (!cart) return false; + for (const item of cart.products) { + if (item.cart_type === 'promotion' && item.selected) return true; + } + return false; + }, [cart]); + + const hasSelected = useMemo(() => { + if (!cart) return false; + for (const item of cart.products) { + if (item.selected) return true; + } + return false; + }, [cart]); + + const hasSelectNoPrice = useMemo(() => { + if (!cart) return false; + for (const item of cart.products) { + if (item.selected && item.price.price_discount == 0) return true; + } + return false; + }, [cart]); + + return ( + <> + <div className={style['title']}>Keranjang Belanja</div> + + <div className='h-6' /> + + <div className={style['content']}> + <div className={style['item-wrapper']}> + <div className={style['item-skeleton']}> + {!cart && <CartItem.Skeleton count={5} height='120px' />} + </div> + + <div className={style['items']}> + {cart?.products.map((item) => ( + <CartItem key={item.id} item={item} /> + ))} + + {cart?.products?.length === 0 && ( + <div className='flex flex-col items-center'> + <Image + src='/images/empty_cart.svg' + alt='Empty Cart' + width={450} + height={450} + /> + <div className='text-title-sm md:text-title-lg text-center font-semibold'> + Keranjangnya masih kosong nih + </div> + <div className='text-body-2 md:text-body-1 text-center mt-3'> + Yuk, tambahin barang-barang yang kamu mau ke keranjang + sekarang! + <br /> + Ada banyak potongan belanjanya pakai kode voucher + </div> + <Link + href='/' + className='btn-solid-red rounded-full text-body-1 mt-6' + > + Mulai Belanja + </Link> + </div> + )} + </div> + </div> + <div + className={`${style['summary-wrapper']} ${ + useDivvice.isMobile && cart?.product_total === 0 ? 'hidden' : '' + }`} + > + <div className={style['summary']}> + {useDivvice.isMobile && ( + <CartSummaryMobile {...summary} isLoaded={!!cart} /> + )} + {!useDivvice.isMobile && ( + <CartSummary {...summary} isLoaded={!!cart} /> + )} + + <div className={isStepApproval ? style['summary-buttons-step-approval'] : style['summary-buttons'] }> + <Tooltip + label={ + hasSelectedPromo && + 'Barang promo tidak dapat dibuat quotation' + } + > + <Button + colorScheme='yellow' + w='full' + isDisabled={hasSelectedPromo || !hasSelected} + as={Link} + href='/shop/quotation' + > + Quotation + </Button> + </Tooltip> + {!isStepApproval && ( + <Tooltip + label={clsxm({ + 'Tidak ada item yang dipilih': !hasSelected, + 'Terdapat item yang tidak ada harga': hasSelectNoPrice, + })} + > + <Button + colorScheme='red' + w='full' + isDisabled={!hasSelected || hasSelectNoPrice} + as={Link} + href='/shop/checkout' + > + Checkout + </Button> + </Tooltip> + )} + </div> + </div> + </div> + </div> + </> + ); +}; + +export default CartPage; diff --git a/src-migrate/pages/shop/product/[slug].tsx b/src-migrate/pages/shop/product/[slug].tsx new file mode 100644 index 00000000..fc72a6b0 --- /dev/null +++ b/src-migrate/pages/shop/product/[slug].tsx @@ -0,0 +1,83 @@ +import { GetServerSideProps, NextPage } from 'next' +import React, { useEffect } from 'react' +import dynamic from 'next/dynamic' +import cookie from 'cookie' + +import { getProductById } from '~/services/product' +import { getIdFromSlug } from '~/libs/slug' +import { IProductDetail } from '~/types/product' + +import { Seo } from '~/components/seo' +import { useRouter } from 'next/router' +import { useProductContext } from '@/contexts/ProductContext' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'), { ssr: false }) +const ProductDetail = dynamic(() => import('~/modules/product-detail'), { ssr: false }) + +type PageProps = { + product: IProductDetail +} + +export const getServerSideProps: GetServerSideProps<PageProps> = (async (context) => { + const { slug } = context.query + const cookieString = context.req.headers.cookie; + const cookies = cookieString ? cookie.parse(cookieString) : {}; + const auth = cookies?.auth ? JSON.parse(cookies.auth) : {}; + const tier = auth?.pricelist || '' + + const productId = getIdFromSlug(slug as string) + + const product = await getProductById(productId, tier) + + if (!product) return { notFound: true } + + return { + props: { product } + } +}) + +const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST + +const ProductDetailPage: NextPage<PageProps> = ({ product }) => { + const router = useRouter(); + + const { setProduct } = useProductContext(); + + useEffect(() => { + if (product) setProduct(product); + }, [product, setProduct]); + + return ( + <BasicLayout> + <Seo + title={`${product.name} - Indoteknik.com`} + description='Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.' + openGraph={{ + url: SELF_HOST + router.asPath, + images: [ + { + url: product?.image, + width: 800, + height: 800, + alt: product?.name, + }, + ], + type: 'product', + }} + additionalMetaTags={[ + { + name: 'keywords', + content: `${product?.name}, Harga ${product?.name}, Beli ${product?.name}, Spesifikasi ${product?.name}`, + } + ]} + canonical={SELF_HOST + router.asPath} + /> + + <div className='md:container pt-4 md:pt-6'> + <ProductDetail product={product} /> + </div> + </BasicLayout> + ) +} + +export default ProductDetailPage
\ No newline at end of file |
