summaryrefslogtreecommitdiff
path: root/src/pages
diff options
context:
space:
mode:
authorIT Fixcomart <it@fixcomart.co.id>2023-03-01 09:18:52 +0000
committerIT Fixcomart <it@fixcomart.co.id>2023-03-01 09:18:52 +0000
commita7abbf4ddc70068620e9f44b74dc162ce2e16ee2 (patch)
tree74f66253717515d364ce74bd8275015c1f829cbc /src/pages
parent90e1edab9b6a8ccc09a49fed3addbec2cbc4e4c3 (diff)
parenta1b9b647a6c4bda1f5db63879639d44543f9557e (diff)
Merged in refactor (pull request #1)
Refactor
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/404.js27
-rw-r--r--src/pages/_app.js31
-rw-r--r--src/pages/_app.jsx42
-rw-r--r--src/pages/_error.js11
-rw-r--r--src/pages/activate.js111
-rw-r--r--src/pages/activate.jsx11
-rw-r--r--src/pages/api/activation-request.js28
-rw-r--r--src/pages/api/activation.js16
-rw-r--r--src/pages/api/login.js15
-rw-r--r--src/pages/api/register.js15
-rw-r--r--src/pages/api/shop/search.js92
-rw-r--r--src/pages/api/shop/suggest.js18
-rw-r--r--src/pages/api/token.js10
-rw-r--r--src/pages/faqs.js91
-rw-r--r--src/pages/faqs.jsx97
-rw-r--r--src/pages/index.js106
-rw-r--r--src/pages/index.jsx36
-rw-r--r--src/pages/login.js97
-rw-r--r--src/pages/login.jsx11
-rw-r--r--src/pages/logout.js14
-rw-r--r--src/pages/my/address/[id]/edit.js249
-rw-r--r--src/pages/my/address/[id]/edit.jsx36
-rw-r--r--src/pages/my/address/create.js234
-rw-r--r--src/pages/my/address/create.jsx13
-rw-r--r--src/pages/my/address/index.js84
-rw-r--r--src/pages/my/address/index.jsx13
-rw-r--r--src/pages/my/invoice/[id].js149
-rw-r--r--src/pages/my/invoice/[id].jsx16
-rw-r--r--src/pages/my/invoices.js180
-rw-r--r--src/pages/my/invoices.jsx13
-rw-r--r--src/pages/my/menu.js82
-rw-r--r--src/pages/my/menu.jsx100
-rw-r--r--src/pages/my/profile.js134
-rw-r--r--src/pages/my/profile.jsx19
-rw-r--r--src/pages/my/transaction/[id].js265
-rw-r--r--src/pages/my/transaction/[id].jsx16
-rw-r--r--src/pages/my/transactions.js198
-rw-r--r--src/pages/my/transactions.jsx15
-rw-r--r--src/pages/my/wishlist.js60
-rw-r--r--src/pages/my/wishlist.jsx13
-rw-r--r--src/pages/register.js100
-rw-r--r--src/pages/register.jsx11
-rw-r--r--src/pages/shop/brands/[slug].js178
-rw-r--r--src/pages/shop/brands/[slug].jsx25
-rw-r--r--src/pages/shop/brands/index.js79
-rw-r--r--src/pages/shop/brands/index.jsx10
-rw-r--r--src/pages/shop/cart.js282
-rw-r--r--src/pages/shop/cart.jsx12
-rw-r--r--src/pages/shop/checkout/finish.js47
-rw-r--r--src/pages/shop/checkout/finish.jsx16
-rw-r--r--src/pages/shop/checkout/index.js325
-rw-r--r--src/pages/shop/checkout/index.jsx13
-rw-r--r--src/pages/shop/product/[slug].js305
-rw-r--r--src/pages/shop/product/[slug].jsx31
-rw-r--r--src/pages/shop/quotation/finish.js39
-rw-r--r--src/pages/shop/quotation/finish.jsx44
-rw-r--r--src/pages/shop/quotation/index.js140
-rw-r--r--src/pages/shop/quotation/index.jsx13
-rw-r--r--src/pages/shop/search.js125
-rw-r--r--src/pages/shop/search.jsx21
60 files changed, 718 insertions, 3866 deletions
diff --git a/src/pages/404.js b/src/pages/404.js
deleted file mode 100644
index 1e1850f2..00000000
--- a/src/pages/404.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Image from "next/image";
-import Link from "@/components/elements/Link";
-import Header from "@/components/layouts/Header";
-import Layout from "@/components/layouts/Layout";
-import PageNotFoundImage from "../images/page-not-found.png";
-
-export default function PageNotFound() {
- return (
- <>
- <Header title="Halaman Tidak Ditemukan - Indoteknik" />
- <Layout>
- <main className="pb-8">
- <Image src={PageNotFoundImage} alt="Halaman Tidak Ditemukan - Indoteknik" className="w-full" />
- <p className="mt-3 h1 text-center">Halaman tidak ditemukan</p>
- <div className="mt-6 flex px-4 gap-x-3">
- <Link href="/" className="btn-light text-gray_r-12 flex-1">
- Kembali ke beranda
- </Link>
- <a href="https://send.whatsapp.com" className="btn-yellow text-gray_r-12 flex-1 h-fit">
- Tanya admin
- </a>
- </div>
- </main>
- </Layout>
- </>
- );
-} \ No newline at end of file
diff --git a/src/pages/_app.js b/src/pages/_app.js
deleted file mode 100644
index 6a40f4e6..00000000
--- a/src/pages/_app.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import '../styles/globals.css';
-import NextProgress from 'next-progress';
-import { useRouter } from 'next/router';
-import { AnimatePresence } from 'framer-motion';
-import { Toaster } from "react-hot-toast";
-
-function MyApp({ Component, pageProps }) {
- const router = useRouter();
-
- return (
- <>
- <Toaster
- position="top-center"
- toastOptions={{
- duration: 3000,
- className: 'border border-gray_r-8'
- }}
- />
- <NextProgress color="#F01C21" options={{ showSpinner: false }} />
- <AnimatePresence
- mode='wait'
- initial={false}
- onExitComplete={() => window.scrollTo(0, 0)}
- >
- <Component {...pageProps} key={router.asPath} />
- </AnimatePresence>
- </>
- )
-}
-
-export default MyApp
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
new file mode 100644
index 00000000..e32efc19
--- /dev/null
+++ b/src/pages/_app.jsx
@@ -0,0 +1,42 @@
+import '../styles/globals.css'
+import NextProgress from 'next-progress'
+import { useRouter } from 'next/router'
+import { AnimatePresence } from 'framer-motion'
+import { Toaster } from 'react-hot-toast'
+import { QueryClient, QueryClientProvider } from 'react-query'
+
+const queryClient = new QueryClient()
+
+function MyApp({ Component, pageProps }) {
+ const router = useRouter()
+
+ return (
+ <>
+ <Toaster
+ position='top-center'
+ toastOptions={{
+ duration: 3000,
+ className: 'border border-gray_r-8'
+ }}
+ />
+ <NextProgress
+ color='#F01C21'
+ options={{ showSpinner: false }}
+ />
+ <QueryClientProvider client={queryClient}>
+ <AnimatePresence
+ mode='wait'
+ initial={false}
+ onExitComplete={() => window.scrollTo(0, 0)}
+ >
+ <Component
+ {...pageProps}
+ key={router.asPath}
+ />
+ </AnimatePresence>
+ </QueryClientProvider>
+ </>
+ )
+}
+
+export default MyApp
diff --git a/src/pages/_error.js b/src/pages/_error.js
deleted file mode 100644
index 107ddf46..00000000
--- a/src/pages/_error.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import Header from "@/components/layouts/Header";
-import Layout from "@/components/layouts/Layout";
-
-export default function Error() {
- return (
- <Layout>
- <Header title="Kesalahan Internal"/>
-
- </Layout>
- );
-} \ No newline at end of file
diff --git a/src/pages/activate.js b/src/pages/activate.js
deleted file mode 100644
index d9b41bf4..00000000
--- a/src/pages/activate.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import axios from "axios";
-import Head from "next/head";
-import Image from "next/image";
-import Link from "@/components/elements/Link";
-import { useRouter } from "next/router";
-import { useEffect, useState } from "react";
-import Alert from "@/components/elements/Alert";
-import Layout from "@/components/layouts/Layout";
-import Spinner from "@/components/elements/Spinner";
-import { setAuth } from "@/core/utils/auth";
-import Logo from "@/images/logo.png";
-
-export default function Activate() {
- const [email, setEmail] = useState('');
- const [isInputFulfilled, setIsInputFulfilled] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [alert, setAlert] = useState();
- const router = useRouter();
- const { token } = router.query;
-
- useEffect(() => {
- if (router.query.email) setEmail(router.query.email);
- }, [router])
-
- useEffect(() => {
- const activateIfTokenExist = async () => {
- if (token) {
- let activation = await axios.post(`${process.env.SELF_HOST}/api/activation`, {token});
- if (activation.data.activation) {
- setAuth(activation.data.user);
- setAlert({
- component: <>Selamat, akun anda berhasil diaktifkan, <Link className="text-gray_r-12" href="/">kembali ke beranda</Link>.</>,
- type: 'success'
- });
- } else {
- setAlert({
- component: <>Mohon maaf token sudah tidak aktif, lakukan permintaan aktivasi akun kembali atau <Link className="text-gray_r-12" href="/login">masuk</Link> jika sudah memiliki akun.</>,
- type: 'info'
- });
- }
- }
- }
- activateIfTokenExist();
- }, [token]);
-
- useEffect(() => {
- setIsInputFulfilled(email != '');
- }, [email]);
-
- const activationRequest = async (e) => {
- e.preventDefault();
- setIsLoading(true);
- let activationRequest = await axios.post(`${process.env.SELF_HOST}/api/activation-request`, {email});
- if (activationRequest.data.activation_request) {
- setAlert({
- component: <>Mohon cek email anda untuk aktivasi akun Indoteknik</>,
- type: 'success'
- });
- } else {
- switch (activationRequest.data.reason) {
- case 'NOT_FOUND':
- setAlert({
- component: <>Email tersebut belum terdaftar, <Link className="text-gray_r-12" href="/register">daftar sekarang</Link>.</>,
- type: 'info'
- });
- break;
- case 'ACTIVE':
- setAlert({
- component: <>Email tersebut sudah terdaftar dan sudah aktif, <Link className="text-gray_r-12" href="/login">masuk sekarang</Link>.</>,
- type: 'info'
- });
- break;
- }
- }
- setIsLoading(false);
- }
- return (
- <>
- <Head>
- <title>Aktivasi Akun Indoteknik</title>
- </Head>
- <Layout className="max-w-lg mx-auto flex flex-col items-center px-4 pb-8">
- <Link href="/" className="mt-16">
- <Image src={Logo} alt="Logo Indoteknik" width={165} height={42} />
- </Link>
- <h1 className="text-2xl text-gray_r-12 mt-4 text-center">Aktivasi Akun Indoteknik Anda</h1>
- <h2 className="text-gray-800 mt-2 mb-4 text-center">Link aktivasi akan dikirimkan melalui email</h2>
- {alert ? (
- <Alert className="text-center" type={alert.type}>{alert.component}</Alert>
- ) : ''}
- <form onSubmit={activationRequest} className="w-full">
- <input
- type="text"
- className="form-input bg-gray-100 mt-4 focus:ring-1 focus:ring-yellow-900"
- placeholder="johndoe@gmail.com"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- autoFocus
- />
- <button type="submit" disabled={!isInputFulfilled} className="btn-yellow font-semibold mt-4 w-full">
- {isLoading ? (
- <div className="flex justify-center items-center gap-x-2">
- <Spinner className="w-4 h-4 text-gray-600 fill-gray-900" /> <span>Loading...</span>
- </div>
- ) : 'Kirim Email'}
- </button>
- </form>
- </Layout>
- </>
- )
-} \ No newline at end of file
diff --git a/src/pages/activate.jsx b/src/pages/activate.jsx
new file mode 100644
index 00000000..cbd10ac2
--- /dev/null
+++ b/src/pages/activate.jsx
@@ -0,0 +1,11 @@
+import SimpleFooter from '@/core/components/elements/Footer/SimpleFooter'
+import ActivateComponent from '@/lib/auth/components/Activate'
+
+export default function Activate() {
+ return (
+ <>
+ <ActivateComponent />
+ <SimpleFooter />
+ </>
+ )
+}
diff --git a/src/pages/api/activation-request.js b/src/pages/api/activation-request.js
index 3f33875c..7fae2fd1 100644
--- a/src/pages/api/activation-request.js
+++ b/src/pages/api/activation-request.js
@@ -1,15 +1,11 @@
-import apiOdoo from "@/core/utils/apiOdoo";
-import mailer from "@/core/utils/mailer";
+import odooApi from '@/core/api/odooApi'
+import mailer from '@/core/utils/mailer'
export default async function handler(req, res) {
try {
- const { email } = req.body;
- let result = await apiOdoo(
- 'POST',
- '/api/v1/user/activation-request',
- {email}
- );
- if (result.activation_request) {
+ const { email } = req.body
+ let result = await odooApi('POST', '/api/v1/user/activation-request', { email })
+ if (result.activationRequest) {
mailer.sendMail({
from: 'sales@indoteknik.com',
to: result.user.email,
@@ -19,13 +15,13 @@ export default async function handler(req, res) {
<br>
<p>Aktivasi akun anda melalui link berikut: <a href="${process.env.SELF_HOST}/activate?token=${result.token}">Aktivasi Akun</a></p>
`
- });
+ })
}
- delete result.user;
- delete result.token;
- res.status(200).json(result);
+ delete result.user
+ delete result.token
+ res.status(200).json(result)
} catch (error) {
- console.log(error);
- res.status(400).json({ error: error.message });
+ console.log(error)
+ res.status(400).json({ error: error.message })
}
-} \ No newline at end of file
+}
diff --git a/src/pages/api/activation.js b/src/pages/api/activation.js
deleted file mode 100644
index 8b22af8d..00000000
--- a/src/pages/api/activation.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import apiOdoo from "@/core/utils/apiOdoo";
-
-export default async function handler(req, res) {
- try {
- const { token } = req.body;
- let result = await apiOdoo(
- 'POST',
- '/api/v1/user/activation',
- {token}
- );
- res.status(200).json(result);
- } catch (error) {
- console.log(error);
- res.status(400).json({ error: error.message });
- }
-} \ No newline at end of file
diff --git a/src/pages/api/login.js b/src/pages/api/login.js
deleted file mode 100644
index e02a73cb..00000000
--- a/src/pages/api/login.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import apiOdoo from "@/core/utils/apiOdoo";
-
-export default async function handler(req, res) {
- try {
- const { email, password } = req.body;
- let result = await apiOdoo(
- 'POST',
- '/api/v1/user/login',
- {email, password}
- );
- res.status(200).json(result);
- } catch (error) {
- res.status(400).json({ error: error.message });
- }
-} \ No newline at end of file
diff --git a/src/pages/api/register.js b/src/pages/api/register.js
deleted file mode 100644
index 7c8d8b39..00000000
--- a/src/pages/api/register.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import apiOdoo from "@/core/utils/apiOdoo";
-
-export default async function handler(req, res) {
- try {
- const { email, name, password } = req.body;
- let result = await apiOdoo(
- 'POST',
- '/api/v1/user/register',
- {email, name, password}
- );
- res.status(200).json(result);
- } catch (error) {
- res.status(400).json({ error: error.message });
- }
-} \ No newline at end of file
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index ad986c86..c1e00d16 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -1,4 +1,5 @@
-import axios from "axios";
+import axios from 'axios'
+import camelcaseObjectDeep from 'camelcase-object-deep'
const productResponseMap = (products) => {
return products.map((product) => {
@@ -7,65 +8,65 @@ const productResponseMap = (products) => {
image: product.image ? product.image[0] : '',
code: product.default_code ? product.default_code[0] : '',
name: product.product_name ? product.product_name[0] : '',
- lowest_price: {
+ lowestPrice: {
price: product.price ? product.price[0] : 0,
- price_discount: product.price_discount ? product.price_discount[0] : 0,
- discount_percentage: product.discount ? product.discount[0] : 0,
+ priceDiscount: product.price_discount ? product.price_discount[0] : 0,
+ discountPercentage: product.discount ? product.discount[0] : 0
},
- variant_total: product.variant_total ? product.variant_total[0] : 0,
- stock_total: product.stock_total ? product.stock_total[0] : 0,
+ variantTotal: product.variant_total ? product.variant_total[0] : 0,
+ stockTotal: product.stock_total ? product.stock_total[0] : 0,
weight: product.weight ? product.weight[0] : 0,
manufacture: {},
- categories: [],
- };
+ categories: []
+ }
if (product.manufacture_id && product.brand) {
productMapped.manufacture = {
id: product.manufacture_id ? product.manufacture_id[0] : '',
- name: product.brand ? product.brand[0] : '',
- };
+ name: product.brand ? product.brand[0] : ''
+ }
}
productMapped.categories = [
{
id: product.category_id ? product.category_id[0] : '',
- name: product.category_name ? product.category_name[0] : '',
+ name: product.category_name ? product.category_name[0] : ''
}
- ];
+ ]
- return productMapped;
- });
+ return productMapped
+ })
}
export default async function handler(req, res) {
- const {
- q,
+ const {
+ q = '*',
page = 1,
brand = '',
category = '',
- price_from = 0,
- price_to = 0,
- order_by = ''
- } = req.query;
+ priceFrom = 0,
+ priceTo = 0,
+ orderBy = ''
+ } = req.query
- let paramOrderBy = '';
- switch (order_by) {
+ let paramOrderBy = ''
+ switch (orderBy) {
case 'price-asc':
- paramOrderBy = ', price_discount ASC';
- break;
+ paramOrderBy = ', price_discount ASC'
+ break
case 'price-desc':
- paramOrderBy = ', price_discount DESC';
- break;
+ paramOrderBy = ', price_discount DESC'
+ break
case 'popular':
- paramOrderBy = ', search_rank DESC';
- break;
+ paramOrderBy = ', search_rank DESC'
+ break
case 'stock':
- paramOrderBy = ', stock_total DESC';
- break;
+ paramOrderBy = ', stock_total DESC'
+ break
}
- let limit = 30;
- let offset = (page - 1) * limit;
+ let limit = 30
+ let offset = (page - 1) * limit
let parameter = [
`facet.query=${q}`,
'facet=true',
@@ -77,20 +78,21 @@ export default async function handler(req, res) {
`start=${offset}`,
`rows=${limit}`,
`sort=product_rating DESC ${paramOrderBy}`,
- `fq=price_discount:[${price_from == '' ? '*' : price_from} TO ${price_to == '' ? '*' : price_to}]`
- ];
+ `fq=price_discount:[${priceFrom == '' ? '*' : priceFrom} TO ${priceTo == '' ? '*' : priceTo}]`
+ ]
- if (brand) parameter.push(`fq=brand:${brand}`);
- if (category) parameter.push(`fq=category_name:${category}`);
-
- let result = await axios(process.env.SOLR_HOST + '/solr/products/select?' + parameter.join('&'));
+ if (brand) parameter.push(`fq=brand:${brand}`)
+ if (category) parameter.push(`fq=category_name:${category}`)
+
+ let result = await axios(process.env.SOLR_HOST + '/solr/products/select?' + parameter.join('&'))
try {
- result.data.response.products = productResponseMap(result.data.response.docs);
- result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start);
- result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows);
- delete result.data.response.docs;
- res.status(200).json(result.data);
+ result.data.response.products = productResponseMap(result.data.response.docs)
+ result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start)
+ result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows)
+ delete result.data.response.docs
+ result.data = camelcaseObjectDeep(result.data)
+ res.status(200).json(result.data)
} catch (error) {
- res.status(400).json({ error: error.message });
+ res.status(400).json({ error: error.message })
}
-} \ No newline at end of file
+}
diff --git a/src/pages/api/shop/suggest.js b/src/pages/api/shop/suggest.js
index 6db1a851..cc0ff2b3 100644
--- a/src/pages/api/shop/suggest.js
+++ b/src/pages/api/shop/suggest.js
@@ -1,12 +1,18 @@
-import axios from "axios";
+import axios from 'axios'
export default async function handler(req, res) {
- const { q } = req.query;
+ const { q = '' } = req.query
- let result = await axios(process.env.SOLR_HOST + `/solr/products/suggest?suggest=true&suggest.dictionary=mySuggester&suggest.q=${q}`);
+ let result = await axios(
+ process.env.SOLR_HOST +
+ `/solr/products/suggest?suggest=true&suggest.dictionary=mySuggester&suggest.q=${q}`
+ )
try {
- res.status(200).json(result.data);
+ res.status(200).json(result.data.suggest.mySuggester[q])
} catch (error) {
- res.status(400).json({ error: error.message });
+ res.status(400).json({
+ numFound: 0,
+ suggestions: []
+ })
}
-} \ No newline at end of file
+}
diff --git a/src/pages/api/token.js b/src/pages/api/token.js
deleted file mode 100644
index ec048158..00000000
--- a/src/pages/api/token.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import axios from "axios";
-
-export default async function handler(req, res) {
- try {
- let result = await axios.get(process.env.ODOO_HOST + '/api/token');
- res.status(200).json(result.data.result);
- } catch (error) {
- res.status(400).json({ error: error.message });
- }
-} \ No newline at end of file
diff --git a/src/pages/faqs.js b/src/pages/faqs.js
deleted file mode 100644
index cdb8ef52..00000000
--- a/src/pages/faqs.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import AppBar from "@/components/layouts/AppBar";
-import Layout from "@/components/layouts/Layout";
-import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
-import { useEffect, useState } from "react";
-
-const dataFaqs = [
- {
- id: 1,
- name: 'Akun',
- description: 'Bantuan tentang pengelolaan fitur dan akun'
- },
- {
- id: 2,
- name: 'Pembelian',
- description: 'Bantuan seputar status stock, layanan pengiriman & asuransi hingga seluruh indonesia'
- },
- {
- id: 3,
- name: 'Metode Pembayaran',
- description: 'Bantuan terkait layanan metode pembayaran'
- },
- {
- id: 4,
- name: 'Quotation',
- description: 'Bantuan fitur RFQ & quotation Express'
- },
- {
- id: 5,
- name: 'Faktur Pajak & Invoice',
- description: 'Bantuan seputar layanan terbit faktur pajak & invoice'
- },
- {
- id: 6,
- name: 'Pengembalian & Garansi',
- description: 'Bantuan cara pengembalian produk & garansi produk'
- }
-];
-
-export default function Faqs() {
- const [ faqs, setFaqs ] = useState([]);
-
- useEffect(() => {
- if (faqs.length == 0) {
- setFaqs(dataFaqs.map((dataFaq) => ({
- ...dataFaq,
- isOpen: false
- })));
- }
- }, [ faqs ]);
-
- const toggleFaq = (id) => {
- const faqsToUpdate = faqs.map(faq => {
- if (faq.id == id) faq.isOpen = !faq.isOpen;
- return faq;
- });
- setFaqs(faqsToUpdate);
- };
-
- return (
- <Layout>
- <AppBar title="FAQ's" />
-
- <div className="divide-y divide-gray_r-6">
- { faqs.map((faq, index) => (
- <div className="p-4" key={index}>
- <div className="flex gap-x-3 items-center">
- <div className="flex-1">
- <p className="font-medium mb-1">{ faq.name }</p>
- <p className="text-caption-1 text-gray_r-11">
- { faq.description }
- </p>
- </div>
- <button type="button" className="p-2 rounded bg-gray_r-4 h-fit" onClick={() => toggleFaq(faq.id)}>
- { faq.isOpen ? (
- <ChevronUpIcon className="w-5"/>
- ) : (
- <ChevronDownIcon className="w-5"/>
- ) }
- </button>
- </div>
- { faq.isOpen && (
- <p className="text-caption-1 text-gray_r-11 leading-7 mt-4">
- { faq?.content || 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.' }
- </p>
- ) }
- </div>
- )) }
- </div>
- </Layout>
- )
-} \ No newline at end of file
diff --git a/src/pages/faqs.jsx b/src/pages/faqs.jsx
new file mode 100644
index 00000000..ddbb4b0a
--- /dev/null
+++ b/src/pages/faqs.jsx
@@ -0,0 +1,97 @@
+import { useEffect, useState } from 'react'
+import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
+import AppLayout from '@/core/components/layouts/AppLayout'
+
+const dataFaqs = [
+ {
+ id: 1,
+ name: 'Akun',
+ description: 'Bantuan tentang pengelolaan fitur dan akun'
+ },
+ {
+ id: 2,
+ name: 'Pembelian',
+ description:
+ 'Bantuan seputar status stock, layanan pengiriman & asuransi hingga seluruh indonesia'
+ },
+ {
+ id: 3,
+ name: 'Metode Pembayaran',
+ description: 'Bantuan terkait layanan metode pembayaran'
+ },
+ {
+ id: 4,
+ name: 'Quotation',
+ description: 'Bantuan fitur RFQ & quotation Express'
+ },
+ {
+ id: 5,
+ name: 'Faktur Pajak & Invoice',
+ description: 'Bantuan seputar layanan terbit faktur pajak & invoice'
+ },
+ {
+ id: 6,
+ name: 'Pengembalian & Garansi',
+ description: 'Bantuan cara pengembalian produk & garansi produk'
+ }
+]
+
+export default function Faqs() {
+ const [faqs, setFaqs] = useState([])
+
+ useEffect(() => {
+ if (faqs.length == 0) {
+ setFaqs(
+ dataFaqs.map((dataFaq) => ({
+ ...dataFaq,
+ isOpen: false
+ }))
+ )
+ }
+ }, [faqs])
+
+ const toggleFaq = (id) => {
+ const faqsToUpdate = faqs.map((faq) => {
+ if (faq.id == id) faq.isOpen = !faq.isOpen
+ return faq
+ })
+ setFaqs(faqsToUpdate)
+ }
+
+ return (
+ <AppLayout title='FAQs'>
+ <div className='divide-y divide-gray_r-6'>
+ {faqs.map((faq, index) => (
+ <div
+ className='p-4'
+ key={index}
+ >
+ <div className='flex gap-x-3 items-center'>
+ <div className='flex-1'>
+ <p className='font-medium mb-1'>{faq.name}</p>
+ <p className='text-caption-1 text-gray_r-11'>{faq.description}</p>
+ </div>
+ <button
+ type='button'
+ className='p-2 rounded bg-gray_r-4 h-fit'
+ onClick={() => toggleFaq(faq.id)}
+ >
+ {faq.isOpen ? (
+ <ChevronUpIcon className='w-5' />
+ ) : (
+ <ChevronDownIcon className='w-5' />
+ )}
+ </button>
+ </div>
+ {faq.isOpen && (
+ <p className='text-caption-1 text-gray_r-11 leading-7 mt-4'>
+ {faq?.content ||
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.'}
+ </p>
+ )}
+ </div>
+ ))}
+ </div>
+ </AppLayout>
+ )
+}
diff --git a/src/pages/index.js b/src/pages/index.js
deleted file mode 100644
index 65999ff6..00000000
--- a/src/pages/index.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import { useEffect, useState } from "react";
-import { Pagination, Autoplay } from "swiper";
-import axios from "axios";
-import { Swiper, SwiperSlide } from "swiper/react";
-import "swiper/css";
-import "swiper/css/pagination";
-import "swiper/css/autoplay";
-
-// Helpers
-import apiOdoo from "@/core/utils/apiOdoo";
-
-// Components
-import Header from "@/components/layouts/Header";
-import ProductSlider from "@/components/products/ProductSlider";
-import Layout from "@/components/layouts/Layout";
-import ManufactureCard from "@/components/manufactures/ManufactureCard";
-import Footer from "@/components/layouts/Footer";
-import Image from "@/components/elements/Image";
-import ProductCategories from "@/components/products/ProductCategories";
-
-const swiperBanner = {
- pagination: { dynamicBullets: true },
- autoplay: {
- delay: 6000,
- disableOnInteraction: false
- },
- modules: [Pagination, Autoplay]
-}
-
-export async function getServerSideProps() {
- const heroBanners = await apiOdoo('GET', `/api/v1/banner?type=index-a-1`);
-
- return { props: { heroBanners } };
-}
-
-export default function Home({ heroBanners }) {
- const [manufactures, setManufactures] = useState(null);
- const [popularProducts, setPopularProducts] = useState(null);
-
- useEffect(() => {
- const getManufactures = async () => {
- const dataManufactures = await apiOdoo('GET', `/api/v1/manufacture?level=prioritas`);
- setManufactures(dataManufactures);
- }
- getManufactures();
-
- const getPopularProducts = async () => {
- const dataPopularProducts = await axios(`${process.env.SELF_HOST}/api/shop/search?q=*&page=1&order_by=popular`);
- setPopularProducts(dataPopularProducts.data.response);
- }
- getPopularProducts();
- }, []);
-
- return (
- <>
- <Header title='Home - Indoteknik' />
- <Layout>
- <Swiper
- slidesPerView={1}
- pagination={swiperBanner.pagination}
- modules={swiperBanner.modules}
- autoplay={swiperBanner.autoplay}
- >
- {
- heroBanners?.map((banner, index) => (
- <SwiperSlide key={index}>
- <Image
- src={banner.image}
- alt={banner.name}
- className="w-full h-auto"
- />
- </SwiperSlide>
- ))
- }
- </Swiper>
- <div className="mt-6 px-4">
- <h2 className="mb-3">Brand Pilihan</h2>
- <Swiper slidesPerView={4} freeMode={true} spaceBetween={16}>
- {
- manufactures?.manufactures?.map((manufacture, index) => (
- <SwiperSlide key={index}>
- <ManufactureCard data={manufacture} key={index} />
- </SwiperSlide>
- ))
- }
- </Swiper>
- </div>
- <div className="my-6 p-4 py-0">
- <h2 className="mb-4">Produk Populer</h2>
- <ProductSlider products={popularProducts} simpleProductTitleLine />
- </div>
-
- <ProductCategories />
-
- <div className="px-4">
- <h5 className="h2 mb-2">Platform Belanja B2B Alat Teknik & Industri di Indonesia</h5>
- <p className="text-gray_r-11 leading-6 text-caption-2 mb-4">
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
- </p>
- </div>
-
- <Footer />
- </Layout>
- </>
- )
-}
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
new file mode 100644
index 00000000..19d3e59c
--- /dev/null
+++ b/src/pages/index.jsx
@@ -0,0 +1,36 @@
+import dynamic from 'next/dynamic'
+import Seo from '@/core/components/Seo'
+import ImageSkeleton from '@/core/components/elements/Skeleton/ImageSkeleton'
+import PopularProductSkeleton from '@/lib/home/components/Skeleton/PopularProductSkeleton'
+
+const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
+
+const HeroBanner = dynamic(() => import('@/lib/home/components/HeroBanner'), {
+ loading: () => <ImageSkeleton />
+})
+
+const PreferredBrand = dynamic(() => import('@/lib/home/components/PreferredBrand'), {
+ loading: () => <PopularProductSkeleton />
+})
+
+const PopularProduct = dynamic(() => import('@/lib/home/components/PopularProduct'), {
+ loading: () => <PopularProductSkeleton />
+})
+
+const CategoryHomeId = dynamic(() => import('@/lib/home/components/CategoryHomeId'), {
+ loading: () => <PopularProductSkeleton />
+})
+
+export default function Home() {
+ return (
+ <BasicLayout>
+ <Seo title='Beranda - Indoteknik' />
+ <HeroBanner />
+ <div className='flex flex-col gap-y-6 my-6'>
+ <PreferredBrand />
+ <PopularProduct />
+ <CategoryHomeId />
+ </div>
+ </BasicLayout>
+ )
+}
diff --git a/src/pages/login.js b/src/pages/login.js
deleted file mode 100644
index e80de44e..00000000
--- a/src/pages/login.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import axios from "axios";
-import Head from "next/head";
-import Image from "next/image";
-import Link from "@/components/elements/Link";
-import { useRouter } from "next/router";
-import { useEffect, useState } from "react";
-import Alert from "@/components/elements/Alert";
-import Layout from "@/components/layouts/Layout";
-import Spinner from "@/components/elements/Spinner";
-import { setAuth } from "@/core/utils/auth";
-import Logo from "@/images/logo.png";
-
-export default function Login() {
- const router = useRouter();
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [isInputFulfilled, setIsInputFulfilled] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [alert, setAlert] = useState();
-
- useEffect(() => {
- setIsInputFulfilled(email && password);
- }, [email, password]);
-
- const login = async (e) => {
- e.preventDefault();
- setIsLoading(true);
- let login = await axios.post(`${process.env.SELF_HOST}/api/login`, {email, password});
- if (login.data.is_auth) {
- setAuth(login.data.user);
- router.push('/');
- } else {
- switch (login.data.reason) {
- case 'NOT_FOUND':
- setAlert({
- component: <>Email atau password tidak cocok</>,
- type: 'info'
- });
- break;
- case 'NOT_ACTIVE':
- setAlert({
- component: <>Email belum diaktivasi, <Link className="text-gray-900" href={`/activate?email=${email}`}>aktivasi sekarang</Link></>,
- type: 'info'
- });
- break;
- }
- setIsLoading(false);
- }
- }
-
- return (
- <>
- <Head>
- <title>Masuk - Indoteknik</title>
- </Head>
- <Layout className="max-w-lg mx-auto flex flex-col items-center px-4 pb-8">
- <Link href="/" className="mt-16">
- <Image src={Logo} alt="Logo Indoteknik" width={165} height={42} />
- </Link>
- <h1 className="text-2xl mt-4 text-center">Mulai Belanja Sekarang</h1>
- <h2 className="text-gray_r-11 font-normal mt-2 mb-4">Masuk ke akun kamu untuk belanja</h2>
- {alert ? (
- <Alert className="text-center" type={alert.type}>{alert.component}</Alert>
- ) : ''}
- <form onSubmit={login} className="w-full">
- <label className="form-label mt-4 mb-2">Alamat Email</label>
- <input
- type="email"
- className="form-input bg-gray_r-2"
- placeholder="johndoe@gmail.com"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- />
- <label className="form-label mt-4 mb-2">Kata Sandi</label>
- <input
- type="password"
- className="form-input bg-gray_r-2"
- placeholder="••••••••"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- />
- <div className="flex justify-end mt-4 w-full">
- <Link href="/forgot-password">Lupa kata sandi</Link>
- </div>
- <button type="submit" disabled={!isInputFulfilled} className="btn-yellow font-semibold mt-4 w-full">
- {isLoading ? (
- <div className="flex justify-center items-center gap-x-2">
- <Spinner className="w-4 h-4 text-gray-600 fill-gray-900" /> <span>Loading...</span>
- </div>
- ) : 'Masuk'}
- </button>
- </form>
- <p className="text-gray-700 mt-4">Belum punya akun Indoteknik? <Link href="/register">Daftar</Link></p>
- </Layout>
- </>
- )
-} \ No newline at end of file
diff --git a/src/pages/login.jsx b/src/pages/login.jsx
new file mode 100644
index 00000000..03509e93
--- /dev/null
+++ b/src/pages/login.jsx
@@ -0,0 +1,11 @@
+import SimpleFooter from '@/core/components/elements/Footer/SimpleFooter'
+import LoginComponent from '@/lib/auth/components/Login'
+
+export default function Login() {
+ return (
+ <>
+ <LoginComponent />
+ <SimpleFooter />
+ </>
+ )
+}
diff --git a/src/pages/logout.js b/src/pages/logout.js
deleted file mode 100644
index 8ea21fab..00000000
--- a/src/pages/logout.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { useRouter } from "next/router";
-import { useEffect } from "react";
-import { deleteAuth } from "@/core/utils/auth";
-
-export default function Logout() {
- const router = useRouter();
-
- useEffect(() => {
- deleteAuth();
- router.replace('/login');
- }, [router]);
-
- return null;
-} \ No newline at end of file
diff --git a/src/pages/my/address/[id]/edit.js b/src/pages/my/address/[id]/edit.js
deleted file mode 100644
index 838d39e7..00000000
--- a/src/pages/my/address/[id]/edit.js
+++ /dev/null
@@ -1,249 +0,0 @@
-import { Controller, useForm } from "react-hook-form"
-import WithAuth from "@/components/auth/WithAuth";
-import Layout from "@/components/layouts/Layout";
-import AppBar from "@/components/layouts/AppBar";
-import { yupResolver } from "@hookform/resolvers/yup";
-import * as Yup from "yup";
-import { Select } from "@/components/elements/Fields";
-import { useEffect, useState } from "react";
-import apiOdoo from "@/core/utils/apiOdoo";
-import { toast } from "react-hot-toast";
-import { useRouter } from "next/router";
-
-const validationSchema = Yup.object().shape({
- type: Yup.string().required('Harus di-pilih'),
- name: Yup.string().min(3, 'Minimal 3 karakter').required('Harus di-isi'),
- email: Yup.string().email('Format harus seperti johndoe@example.com').required('Harus di-isi'),
- mobile: Yup.string().required('Harus di-isi'),
- street: Yup.string().required('Harus di-isi'),
- zip: Yup.string().required('Harus di-isi'),
- city: Yup.string().required('Harus di-pilih'),
-});
-
-const types = [
- { value: 'contact', label: 'Contact Address' },
- { value: 'invoice', label: 'Invoice Address' },
- { value: 'delivery', label: 'Delivery Address' },
- { value: 'other', label: 'Other Address' },
-];
-
-export async function getServerSideProps( context ) {
- const { id } = context.query;
- const address = await apiOdoo('GET', `/api/v1/partner/${id}/address`);
- let defaultValues = {
- type: address.type,
- name: address.name,
- email: address.email,
- mobile: address.mobile,
- street: address.street,
- zip: address.zip,
- city: address.city?.id || '',
- oldDistrict: address.district?.id || '',
- district: '',
- oldSubDistrict: address.sub_district?.id || '',
- subDistrict: '',
- };
- return { props: { id, defaultValues } };
-}
-
-export default function EditAddress({ id, defaultValues }) {
- const router = useRouter();
- const {
- register,
- formState: { errors },
- handleSubmit,
- watch,
- setValue,
- getValues,
- control,
- } = useForm({
- resolver: yupResolver(validationSchema),
- defaultValues
- });
-
- const [ cities, setCities ] = useState([]);
- const [ districts, setDistricts ] = useState([]);
- const [ subDistricts, setSubDistricts ] = useState([]);
-
- useEffect(() => {
- const loadCities = async () => {
- let dataCities = await apiOdoo('GET', '/api/v1/city');
- dataCities = dataCities.map((city) => ({ value: city.id, label: city.name }));
- setCities(dataCities);
- };
- loadCities();
- }, []);
-
- const watchCity = watch('city');
- useEffect(() => {
- setValue('district', '');
- if (watchCity) {
- const loadDistricts = async () => {
- let dataDistricts = await apiOdoo('GET', `/api/v1/district?city_id=${watchCity}`);
- dataDistricts = dataDistricts.map((district) => ({ value: district.id, label: district.name }));
- setDistricts(dataDistricts);
- let oldDistrict = getValues('oldDistrict');
- if (oldDistrict) {
- setValue('district', oldDistrict);
- setValue('oldDistrict', '');
- }
- };
- loadDistricts();
- }
- }, [ watchCity, setValue, getValues ]);
-
- const watchDistrict = watch('district');
- useEffect(() => {
- setValue('subDistrict', '');
- if (watchDistrict) {
- const loadSubDistricts = async () => {
- let dataSubDistricts = await apiOdoo('GET', `/api/v1/sub_district?district_id=${watchDistrict}`);
- dataSubDistricts = dataSubDistricts.map((district) => ({ value: district.id, label: district.name }));
- setSubDistricts(dataSubDistricts);
- let oldSubDistrict = getValues('oldSubDistrict');
- if (oldSubDistrict) {
- setValue('subDistrict', oldSubDistrict);
- setValue('oldSubDistrict', '');
- }
- };
- loadSubDistricts();
- }
- }, [ watchDistrict, setValue, getValues ])
-
- const onSubmitHandler = async (values) => {
- const parameters = {
- ...values,
- city_id: values.city,
- district_id: values.district,
- sub_district_id: values.subDistrict
- }
-
- const address = await apiOdoo('PUT', `/api/v1/partner/${id}/address`, parameters);
- if (address?.id) {
- toast.success('Berhasil mengubah alamat');
- router.back();
- }
- };
-
- return (
- <WithAuth>
- <Layout>
- <AppBar title="Ubah Alamat" />
-
- <form className="p-4 flex flex-col gap-y-4" onSubmit={handleSubmit(onSubmitHandler)}>
- <div>
- <label className="form-label mb-2">Label Alamat</label>
- <Controller
- name="type"
- control={control}
- render={props => <Select {...props} isSearchable={false} options={types} />}
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.type?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Nama</label>
- <input
- {...register('name')}
- placeholder="John Doe"
- type="text"
- className="form-input"
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.name?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Email</label>
- <input
- {...register('email')}
- placeholder="johndoe@example.com"
- type="email"
- className="form-input"
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.email?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Mobile</label>
- <input
- {...register('mobile')}
- placeholder="08xxxxxxxx"
- type="tel"
- className="form-input"
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.mobile?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Alamat</label>
- <input
- {...register('street')}
- placeholder="Jl. Bandengan Utara 85A"
- type="text"
- className="form-input"
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.street?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Kode Pos</label>
- <input
- {...register('zip')}
- placeholder="10100"
- type="number"
- className="form-input"
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.zip?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Kota</label>
- <Controller
- name="city"
- control={control}
- render={props => <Select {...props} options={cities} />}
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.city?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Kecamatan</label>
- <Controller
- name="district"
- control={control}
- render={props => (
- <Select
- {...props}
- options={districts}
- disabled={!watchCity}
- />
- )}
- />
- </div>
-
- <div>
- <label className="form-label mb-2">Kelurahan</label>
- <Controller
- name="subDistrict"
- control={control}
- render={props => (
- <Select
- {...props}
- options={subDistricts}
- disabled={!watchDistrict}
- />
- )}
- />
- </div>
-
- <button
- type="submit"
- className="btn-yellow mt-2 w-full"
- >
- Simpan
- </button>
- </form>
- </Layout>
- </WithAuth>
- )
-} \ No newline at end of file
diff --git a/src/pages/my/address/[id]/edit.jsx b/src/pages/my/address/[id]/edit.jsx
new file mode 100644
index 00000000..bc5f3471
--- /dev/null
+++ b/src/pages/my/address/[id]/edit.jsx
@@ -0,0 +1,36 @@
+import AppLayout from '@/core/components/layouts/AppLayout'
+import addressApi from '@/lib/address/api/addressApi'
+import EditAddressComponent from '@/lib/address/components/EditAddress'
+import IsAuth from '@/lib/auth/components/IsAuth'
+
+export default function EditAddress({ id, defaultValues }) {
+ return (
+ <IsAuth>
+ <AppLayout title='Ubah Alamat'>
+ <EditAddressComponent
+ id={id}
+ defaultValues={defaultValues}
+ />
+ </AppLayout>
+ </IsAuth>
+ )
+}
+
+export async function getServerSideProps(context) {
+ const { id } = context.query
+ const address = await addressApi({ id })
+ const defaultValues = {
+ type: address.type,
+ name: address.name,
+ email: address.email,
+ mobile: address.mobile,
+ street: address.street,
+ zip: address.zip,
+ city: address.city?.id || '',
+ oldDistrict: address.district?.id || '',
+ district: '',
+ oldSubDistrict: address.subDistrict?.id || '',
+ subDistrict: ''
+ }
+ return { props: { id, defaultValues } }
+}
diff --git a/src/pages/my/address/create.js b/src/pages/my/address/create.js
deleted file mode 100644
index 42cd117c..00000000
--- a/src/pages/my/address/create.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import { Controller, useForm } from "react-hook-form"
-import WithAuth from "@/components/auth/WithAuth";
-import Layout from "@/components/layouts/Layout";
-import AppBar from "@/components/layouts/AppBar";
-import { yupResolver } from "@hookform/resolvers/yup";
-import * as Yup from "yup";
-import { Select } from "@/components/elements/Fields";
-import { useEffect, useState } from "react";
-import apiOdoo from "@/core/utils/apiOdoo";
-import { useAuth } from "@/core/utils/auth";
-import { toast } from "react-hot-toast";
-import { useRouter } from "next/router";
-
-const validationSchema = Yup.object().shape({
- type: Yup.string().required('Harus di-pilih'),
- name: Yup.string().min(3, 'Minimal 3 karakter').required('Harus di-isi'),
- email: Yup.string().email('Format harus seperti johndoe@example.com').required('Harus di-isi'),
- mobile: Yup.string().required('Harus di-isi'),
- street: Yup.string().required('Harus di-isi'),
- zip: Yup.string().required('Harus di-isi'),
- city: Yup.string().required('Harus di-pilih'),
-});
-
-const defaultValues = {
- type: '',
- name: '',
- email: '',
- mobile: '',
- street: '',
- city: '',
- district: '',
- subDistrict: '',
- zip: '',
-};
-
-const types = [
- { value: 'contact', label: 'Contact Address' },
- { value: 'invoice', label: 'Invoice Address' },
- { value: 'delivery', label: 'Delivery Address' },
- { value: 'other', label: 'Other Address' },
-];
-
-export default function CreateAddress() {
- const [ auth ] = useAuth();
- const router = useRouter();
- const {
- register,
- formState: { errors },
- handleSubmit,
- watch,
- setValue,
- control,
- } = useForm({
- resolver: yupResolver(validationSchema),
- defaultValues
- });
-
- const [ cities, setCities ] = useState([]);
- const [ districts, setDistricts ] = useState([]);
- const [ subDistricts, setSubDistricts ] = useState([]);
-
- useEffect(() => {
- const loadCities = async () => {
- let dataCities = await apiOdoo('GET', '/api/v1/city');
- dataCities = dataCities.map((city) => ({ value: city.id, label: city.name }));
- setCities(dataCities);
- };
- loadCities();
- }, []);
-
- const watchCity = watch('city');
- useEffect(() => {
- setValue('district', '');
- if (watchCity) {
- const loadDistricts = async () => {
- let dataDistricts = await apiOdoo('GET', `/api/v1/district?city_id=${watchCity}`);
- dataDistricts = dataDistricts.map((district) => ({ value: district.id, label: district.name }));
- setDistricts(dataDistricts);
- };
- loadDistricts();
- }
- }, [ watchCity, setValue ]);
-
- const watchDistrict = watch('district');
- useEffect(() => {
- setValue('subDistrict', '');
- if (watchDistrict) {
- const loadSubDistricts = async () => {
- let dataSubDistricts = await apiOdoo('GET', `/api/v1/sub_district?district_id=${watchDistrict}`);
- dataSubDistricts = dataSubDistricts.map((district) => ({ value: district.id, label: district.name }));
- setSubDistricts(dataSubDistricts);
- };
- loadSubDistricts();
- }
- }, [ watchDistrict, setValue ])
-
- const onSubmitHandler = async (values) => {
- const parameters = {
- ...values,
- city_id: values.city,
- district_id: values.district,
- sub_district_id: values.subDistrict,
- parent_id: auth.partner_id
- };
-
- const address = await apiOdoo('POST', '/api/v1/partner/address', parameters);
- if (address?.id) {
- toast.success('Berhasil menambahkan alamat');
- router.back();
- }
- };
-
- return (
- <WithAuth>
- <Layout>
- <AppBar title="Tambah Alamat" />
-
- <form className="p-4 flex flex-col gap-y-4" onSubmit={handleSubmit(onSubmitHandler)}>
- <div>
- <label className="form-label mb-2">Label Alamat</label>
- <Controller
- name="type"
- control={control}
- render={props => <Select {...props} isSearchable={false} options={types} />}
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.type?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Nama</label>
- <input
- {...register('name')}
- placeholder="John Doe"
- type="text"
- className="form-input"
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.name?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Email</label>
- <input
- {...register('email')}
- placeholder="johndoe@example.com"
- type="email"
- className="form-input"
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.email?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Mobile</label>
- <input
- {...register('mobile')}
- placeholder="08xxxxxxxx"
- type="tel"
- className="form-input"
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.mobile?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Alamat</label>
- <input
- {...register('street')}
- placeholder="Jl. Bandengan Utara 85A"
- type="text"
- className="form-input"
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.street?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Kode Pos</label>
- <input
- {...register('zip')}
- placeholder="10100"
- type="number"
- className="form-input"
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.zip?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Kota</label>
- <Controller
- name="city"
- control={control}
- render={props => <Select {...props} options={cities} />}
- />
- <div className="text-caption-2 text-red_r-11 mt-1">{ errors.city?.message }</div>
- </div>
-
- <div>
- <label className="form-label mb-2">Kecamatan</label>
- <Controller
- name="district"
- control={control}
- render={props => (
- <Select
- {...props}
- options={districts}
- disabled={!watchCity}
- />
- )}
- />
- </div>
-
- <div>
- <label className="form-label mb-2">Kelurahan</label>
- <Controller
- name="subDistrict"
- control={control}
- render={props => (
- <Select
- {...props}
- options={subDistricts}
- disabled={!watchDistrict}
- />
- )}
- />
- </div>
-
- <button
- type="submit"
- className="btn-yellow mt-2 w-full"
- >
- Simpan
- </button>
- </form>
- </Layout>
- </WithAuth>
- )
-} \ No newline at end of file
diff --git a/src/pages/my/address/create.jsx b/src/pages/my/address/create.jsx
new file mode 100644
index 00000000..ec17f987
--- /dev/null
+++ b/src/pages/my/address/create.jsx
@@ -0,0 +1,13 @@
+import AppLayout from '@/core/components/layouts/AppLayout'
+import CreateAddressComponent from '@/lib/address/components/CreateAddress'
+import IsAuth from '@/lib/auth/components/IsAuth'
+
+export default function CreateAddress() {
+ return (
+ <IsAuth>
+ <AppLayout title='Tambah Alamat'>
+ <CreateAddressComponent />
+ </AppLayout>
+ </IsAuth>
+ )
+}
diff --git a/src/pages/my/address/index.js b/src/pages/my/address/index.js
deleted file mode 100644
index 5cad4410..00000000
--- a/src/pages/my/address/index.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import { useEffect, useState } from "react";
-import { useRouter } from "next/router";
-
-import AppBar from "@/components/layouts/AppBar";
-import Layout from "@/components/layouts/Layout";
-import Link from "@/components/elements/Link";
-import WithAuth from "@/components/auth/WithAuth";
-
-import apiOdoo from "@/core/utils/apiOdoo";
-import { useAuth } from "@/core/utils/auth";
-import { createOrUpdateItemAddress, getItemAddress } from "@/core/utils/address";
-import { toast } from "react-hot-toast";
-
-export default function Address() {
- const router = useRouter();
- const { select } = router.query;
- const [ auth ] = useAuth();
- const [ addresses, setAddresses ] = useState(null);
- const [ selectedAdress, setSelectedAdress ] = useState(null);
-
- useEffect(() => {
- const getAddress = async () => {
- if (auth) {
- const dataAddress = await apiOdoo('GET', `/api/v1/user/${auth.id}/address`);
- setAddresses(dataAddress);
- }
- };
- getAddress();
- }, [auth]);
-
- useEffect(() => {
- if (select) {
- setSelectedAdress(getItemAddress(select));
- }
- }, [select]);
-
- const changeSelectedAddress = (id) => {
- if (select) {
- createOrUpdateItemAddress(select, id);
- router.back();
- }
- };
-
- return (
- <WithAuth>
- <Layout>
- <AppBar title="Daftar Alamat" />
-
- <div className="text-right mt-4 px-4">
- <Link href="/my/address/create">Tambah Alamat</Link>
- </div>
-
- <div className="grid gap-y-4 p-4">
- { auth && addresses && addresses.map((address, index) => {
- let type = address.type.charAt(0).toUpperCase() + address.type.slice(1) + ' Address';
- return (
- <div
- key={index}
- className={"p-4 rounded-md border " + (selectedAdress && selectedAdress == address.id ? "bg-gray_r-4" : "border-gray_r-7") }
- >
- <div onClick={() => changeSelectedAddress(address.id)}>
- <div className="flex gap-x-2" >
- <div className="badge-red">{ type }</div>
- { auth?.partner_id == address.id && (
- <div className="badge-green">Utama</div>
- ) }
- </div>
- <p className="font-medium mt-2">{ address.name }</p>
- { address.mobile && (
- <p className="mt-2 text-gray_r-11">{ address.mobile }</p>
- ) }
- <p className={`mt-1 leading-6 ${selectedAdress && selectedAdress == address.id ? "text-gray_r-12" : "text-gray_r-11"}`}>
- { address.street }
- </p>
- </div>
- <Link href={`/my/address/${address.id}/edit`} className="btn-light bg-white mt-3 w-full text-gray_r-11">Ubah Alamat</Link>
- </div>
- );
- }) }
- </div>
- </Layout>
- </WithAuth>
- )
-} \ No newline at end of file
diff --git a/src/pages/my/address/index.jsx b/src/pages/my/address/index.jsx
new file mode 100644
index 00000000..93ed40b0
--- /dev/null
+++ b/src/pages/my/address/index.jsx
@@ -0,0 +1,13 @@
+import AppLayout from '@/core/components/layouts/AppLayout'
+import AddressesComponent from '@/lib/address/components/Addresses'
+import IsAuth from '@/lib/auth/components/IsAuth'
+
+export default function Addresses() {
+ return (
+ <IsAuth>
+ <AppLayout title='Daftar Alamat'>
+ <AddressesComponent />
+ </AppLayout>
+ </IsAuth>
+ )
+}
diff --git a/src/pages/my/invoice/[id].js b/src/pages/my/invoice/[id].js
deleted file mode 100644
index 820c9af8..00000000
--- a/src/pages/my/invoice/[id].js
+++ /dev/null
@@ -1,149 +0,0 @@
-import AppBar from "@/components/layouts/AppBar";
-import Layout from "@/components/layouts/Layout";
-import LineDivider from "@/components/elements/LineDivider";
-import WithAuth from "@/components/auth/WithAuth";
-import { useEffect, useState } from "react";
-import apiOdoo from "@/core/utils/apiOdoo";
-import { useRouter } from "next/router";
-import { useAuth } from "@/core/utils/auth";
-import VariantCard from "@/components/variants/VariantCard";
-import currencyFormat from "@/core/utils/currencyFormat";
-import Disclosure from "@/components/elements/Disclosure";
-import DescriptionRow from "@/components/elements/DescriptionRow";
-import { SkeletonList } from "@/components/elements/Skeleton";
-import VariantGroupCard from "@/components/variants/VariantGroupCard";
-
-export default function DetailInvoice() {
- const router = useRouter();
- const { id } = router.query;
- const [ auth ] = useAuth();
- const [ invoice, setInvoice ] = useState(null);
-
- useEffect(() => {
- if (auth && id) {
- const loadInvoice = async () => {
- const dataInvoice = await apiOdoo('GET', `/api/v1/partner/${auth?.partner_id}/invoice/${id}`);
- setInvoice(dataInvoice);
- }
- loadInvoice();
- }
- }, [ auth, id ]);
-
- const Customer = () => {
- const customer = invoice?.customer;
- const fullAddress = [];
- if (customer?.street) fullAddress.push(customer.street);
- if (customer?.sub_district?.name) fullAddress.push(customer.sub_district.name);
- if (customer?.district?.name) fullAddress.push(customer.district.name);
- if (customer?.city?.name) fullAddress.push(customer.city.name);
-
- return (
- <div className="p-4 pt-0 flex flex-col gap-y-4">
- <DescriptionRow label="Nama">{ invoice?.customer?.name }</DescriptionRow>
- <DescriptionRow label="Email">{ invoice?.customer?.email || '-' }</DescriptionRow>
- <DescriptionRow label="No Telepon">{ invoice?.customer?.mobile || '-' }</DescriptionRow>
- <DescriptionRow label="Alamat">{ fullAddress.join(', ') }</DescriptionRow>
- </div>
- );
- };
-
- const downloadTaxInvoice = () => {
- window.open(`${process.env.ODOO_HOST}/api/v1/download/tax-invoice/${invoice.id}/${invoice.token}`, 'Download')
- }
-
- const downloadInvoice = () => {
- window.open(`${process.env.ODOO_HOST}/api/v1/download/invoice/${invoice.id}/${invoice.token}`, 'Download')
- }
-
- return (
- <WithAuth>
- <Layout className="pb-4">
- <AppBar title="Detail Invoice" />
-
- { invoice ? (
- <>
- <div className="p-4 flex flex-col gap-y-4">
- <DescriptionRow label="No Invoice">
- { invoice?.name }
- </DescriptionRow>
- <DescriptionRow label="Status Transaksi">
- { invoice?.amount_residual > 0 ? (
- <span className="badge-solid-red">Belum Lunas</span>
- ) : (
- <span className="badge-solid-green">Lunas</span>
- ) }
- </DescriptionRow>
- <DescriptionRow label="Purchase Order">
- { invoice?.purchase_order_name || '-' }
- </DescriptionRow>
- <DescriptionRow label="Ketentuan Pembayaran">
- { invoice?.payment_term }
- </DescriptionRow>
- { invoice?.amount_residual > 0 && invoice.invoice_date != invoice.invoice_date_due && (
- <DescriptionRow label="Tanggal Jatuh Tempo">
- { invoice?.invoice_date_due }
- </DescriptionRow>
- ) }
- <DescriptionRow label="Nama Sales">
- { invoice?.sales }
- </DescriptionRow>
- <DescriptionRow label="Tanggal Invoice">
- { invoice?.invoice_date }
- </DescriptionRow>
- <div className="flex items-center">
- <p className="text-gray_r-11 leading-none">Faktur Pembelian</p>
- <button
- type="button"
- className="btn-light py-1.5 px-3 ml-auto"
- onClick={downloadInvoice}
- >
- Download
- </button>
- </div>
- <div className="flex items-center">
- <p className="text-gray_r-11 leading-none">Faktur Pajak</p>
- <button
- type="button"
- className="btn-light py-1.5 px-3 ml-auto"
- onClick={downloadTaxInvoice}
- disabled={!invoice.efaktur}
- >
- Download
- </button>
- </div>
- </div>
-
- <LineDivider />
-
- <Disclosure
- label="Detail Penagihan"
- />
-
- <Customer />
-
- <LineDivider />
-
- <Disclosure
- label="Detail Produk"
- />
-
- <div className="mt-2 p-4 pt-0 flex flex-col gap-y-3">
- <VariantGroupCard
- variants={invoice?.products}
- buyMore
- />
- <div className="flex justify-between mt-3 font-medium">
- <p className="text-gray_r-11">Total Belanja</p>
- <p>{ currencyFormat(invoice?.amount_total || 0) }</p>
- </div>
- </div>
- </>
- ) : (
- <div className="p-4 py-6">
- <SkeletonList number={12} />
- </div>
- ) }
- </Layout>
- </WithAuth>
- );
-} \ No newline at end of file
diff --git a/src/pages/my/invoice/[id].jsx b/src/pages/my/invoice/[id].jsx
new file mode 100644
index 00000000..4938d8f8
--- /dev/null
+++ b/src/pages/my/invoice/[id].jsx
@@ -0,0 +1,16 @@
+import AppLayout from '@/core/components/layouts/AppLayout'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import InvoiceComponent from '@/lib/invoice/components/Invoice'
+import { useRouter } from 'next/router'
+
+export default function Invoice() {
+ const router = useRouter()
+
+ return (
+ <IsAuth>
+ <AppLayout title='Invoice & Faktur Pajak'>
+ <InvoiceComponent id={router.query.id} />
+ </AppLayout>
+ </IsAuth>
+ )
+}
diff --git a/src/pages/my/invoices.js b/src/pages/my/invoices.js
deleted file mode 100644
index 9b2e77dc..00000000
--- a/src/pages/my/invoices.js
+++ /dev/null
@@ -1,180 +0,0 @@
-import WithAuth from "@/components/auth/WithAuth"
-import Alert from "@/components/elements/Alert"
-import Link from "@/components/elements/Link"
-import Pagination from "@/components/elements/Pagination"
-import AppBar from "@/components/layouts/AppBar"
-import Layout from "@/components/layouts/Layout"
-import apiOdoo from "@/core/utils/apiOdoo"
-import { useAuth } from "@/core/utils/auth"
-import currencyFormat from "@/core/utils/currencyFormat"
-import useBottomPopup from "@/lib/elements/hooks/useBottomPopup"
-import { CheckIcon, ClockIcon, EllipsisVerticalIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"
-import { useRouter } from "next/router"
-import { useEffect, useRef, useState } from "react"
-
-export default function Invoices() {
- const [ auth ] = useAuth()
- const router = useRouter()
- const {
- q,
- page = 1
- } = router.query
-
- const [ invoices, setInvoices ] = useState([])
-
- const [ pageCount, setPageCount ] = useState(0)
- const [ isLoading, setIsLoading ] = useState(true)
-
- const searchQueryRef = useRef()
-
- useEffect(() => {
- const loadInvoices = async () => {
- if (auth) {
- const limit = 10
- let offset = (page - 1) * 10
- let queryParams = [`limit=${limit}`, `offset=${offset}`]
- if (q) queryParams.push(`name=${q}`)
- queryParams = queryParams.join('&')
- queryParams = queryParams ? '?' + queryParams : ''
-
- const dataInvoices = await apiOdoo('GET', `/api/v1/partner/${auth.partner_id}/invoice${queryParams}`)
- setInvoices(dataInvoices)
- setPageCount(Math.ceil(dataInvoices.sale_order_total / limit))
- setIsLoading(false)
- }
- }
- loadInvoices()
- }, [ auth, q, page ])
-
- const actionSearch = (e) => {
- e.preventDefault()
- let queryParams = []
- if (searchQueryRef.current.value) queryParams.push(`q=${searchQueryRef.current.value}`)
- queryParams = queryParams.join('&')
- queryParams = queryParams ? `?${queryParams}` : ''
- router.push(`/my/invoices${queryParams}`)
- }
-
- const downloadInvoice = (data) => {
- const url = `${process.env.ODOO_HOST}/api/v1/download/invoice/${data.id}/${data.token}`
- window.open(url, 'download')
- closePopup()
- }
-
- const downloadTaxInvoice = (data) => {
- const url = `${process.env.ODOO_HOST}/api/v1/download/tax-invoice/${data.id}/${data.token}`
- window.open(url, 'download')
- closePopup()
- }
-
- const childrenPopup = (data) => (
- <div className="flex flex-col gap-y-6">
- <button
- className="text-left disabled:opacity-60"
- onClick={() => downloadInvoice(data)}
- >
- Download Faktur Pembelian
- </button>
- <button
- className="text-left disabled:opacity-60"
- disabled={!data?.efaktur}
- onClick={() => downloadTaxInvoice(data)}
- >
- Download Faktur Pajak
- </button>
- </div>
- )
-
- const {
- closePopup,
- openPopup,
- BottomPopup
- } = useBottomPopup({
- title: 'Lainnya',
- children: childrenPopup
- })
-
- return (
- <WithAuth>
- <Layout>
- <AppBar title="Invoice" />
-
- <form onSubmit={actionSearch} className="p-4 pb-0 flex gap-x-4">
- <input
- type="text"
- className="form-input"
- placeholder="Cari Transaksi"
- ref={searchQueryRef}
- defaultValue={q}
- />
- <button type="submit" className="border border-gray_r-7 rounded px-3">
- <MagnifyingGlassIcon className="w-5"/>
- </button>
- </form>
-
- <div className="p-4 flex flex-col gap-y-5">
- { invoices?.invoice_total === 0 && !isLoading && (
- <Alert type="info" className="text-center">
- Invoice tidak ditemukan
- </Alert>
- ) }
- { invoices?.invoices?.map((invoice, index) => (
- <div className="p-4 shadow border border-gray_r-3 rounded-md" key={index}>
- <div className="grid grid-cols-2">
- <Link href={`/my/invoice/${invoice.id}`}>
- <span className="text-caption-2 text-gray_r-11">No. Invoice</span>
- <h2 className="text-red_r-11 mt-1">{ invoice.name }</h2>
- </Link>
- <div className="flex gap-x-1 justify-end">
- { invoice.amount_residual > 0 ? (
- <div className="badge-solid-red h-fit ml-auto">Belum Lunas</div>
- ) : (
- <div className="badge-solid-green h-fit ml-auto">Lunas</div>
- ) }
- <EllipsisVerticalIcon className="w-5 h-5" onClick={() => openPopup(invoice)} />
- </div>
- </div>
- <Link href={`/my/invoice/${invoice.id}`}>
- <div className="grid grid-cols-2 text-caption-2 text-gray_r-11 mt-2 font-normal">
- <p>
- { invoice.invoice_date }
- </p>
- <p className="text-right">
- { invoice.payment_term }
- </p>
- </div>
- <hr className="my-3"/>
- <div className="grid grid-cols-2">
- <div>
- <span className="text-caption-2 text-gray_r-11">No. Purchase Order</span>
- <p className="mt-1 font-medium text-gray_r-12">{ invoice.purchase_order_name || '-' }</p>
- </div>
- <div className="text-right">
- <span className="text-caption-2 text-gray_r-11">Total Invoice</span>
- <p className="mt-1 font-medium text-gray_r-12">{ currencyFormat(invoice.amount_total) }</p>
- </div>
- </div>
- </Link>
- { invoice.efaktur ? (
- <div className="badge-green h-fit mt-3 ml-auto flex items-center gap-x-0.5">
- <CheckIcon className="w-4 stroke-2" />
- Faktur Pajak
- </div>
- ) : (
- <div className="badge-red h-fit mt-3 ml-auto flex items-center gap-x-0.5">
- <ClockIcon className="w-4 stroke-2" />
- Faktur Pajak
- </div>
- ) }
- </div>
- )) }
- </div>
-
- <div className="pb-6 pt-2">
- <Pagination currentPage={page} pageCount={pageCount} url={`/my/invoices${q ? `?q=${q}` : ''}`} />
- </div>
- { BottomPopup }
- </Layout>
- </WithAuth>
- )
-} \ No newline at end of file
diff --git a/src/pages/my/invoices.jsx b/src/pages/my/invoices.jsx
new file mode 100644
index 00000000..12a5ff7e
--- /dev/null
+++ b/src/pages/my/invoices.jsx
@@ -0,0 +1,13 @@
+import AppLayout from '@/core/components/layouts/AppLayout'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import InvoicesComponent from '@/lib/invoice/components/Invoices'
+
+export default function Invoices() {
+ return (
+ <IsAuth>
+ <AppLayout title='Invoice & Faktur Pajak'>
+ <InvoicesComponent />
+ </AppLayout>
+ </IsAuth>
+ )
+}
diff --git a/src/pages/my/menu.js b/src/pages/my/menu.js
deleted file mode 100644
index ae6c2af8..00000000
--- a/src/pages/my/menu.js
+++ /dev/null
@@ -1,82 +0,0 @@
-
-import AppBar from "@/components/layouts/AppBar";
-import Layout from "@/components/layouts/Layout";
-import Link from "@/components/elements/Link";
-import { useAuth } from "@/core/utils/auth";
-import {
- ArrowRightOnRectangleIcon,
- ChatBubbleLeftRightIcon,
- ChevronRightIcon,
- MapIcon,
- PaperClipIcon,
- PencilSquareIcon,
- QuestionMarkCircleIcon,
- ReceiptPercentIcon,
- UserIcon,
- HeartIcon
-} from "@heroicons/react/24/outline";
-import WithAuth from "@/components/auth/WithAuth";
-
-const Menu = ({ icon, name, url }) => {
- return (
- <Link href={url} className="text-gray_r-11 font-normal flex gap-x-2 items-center py-4 border-b border-gray_r-6">
- <span className="flex gap-x-2">
- { icon }
- { name }
- </span>
- <ChevronRightIcon className="w-5 ml-auto"/>
- </Link>
- );
-};
-
-export default function MyMenu() {
- const [auth] = useAuth();
-
- return (
- <WithAuth>
- <Layout>
- <AppBar title="Menu Utama" />
-
- <div className="p-4 flex gap-x-2 items-center">
- <div className="flex-1 flex gap-x-3 items-center">
- <div className="p-2 bg-gray_r-4 rounded-full h-fit">
- <UserIcon className="w-6" />
- </div>
- <div>
- <h2>{ auth?.name }</h2>
- { auth?.company ? (
- <div className="badge-red font-normal text-xs">Akun Bisnis</div>
- ) : (
- <div className="badge-gray font-normal text-xs">Akun Individu</div>
- ) }
- </div>
- </div>
- <Link href="/my/profile">
- <PencilSquareIcon className="w-6 text-yellow_r-12"/>
- </Link>
- </div>
-
- <div className="px-4 mt-4">
- <p className="font-medium mb-2">Aktivitas Pembelian</p>
- <div className="flex flex-col mb-6">
- <Menu icon={<ReceiptPercentIcon className="w-5" />} name="Daftar Transaksi" url="/my/transactions" />
- <Menu icon={<PaperClipIcon className="w-5" />} name="Invoice & Faktur Pajak" url="/my/invoices" />
- <Menu icon={<HeartIcon className="w-5" />} name="Wishlist" url="/my/wishlist" />
- </div>
-
- <p className="font-medium mb-2">Pusat Bantuan</p>
- <div className="flex flex-col mb-6">
- <Menu icon={<ChatBubbleLeftRightIcon className="w-5"/>} name="Layanan Pelanggan" url="/" />
- <Menu icon={<QuestionMarkCircleIcon className="w-5"/>} name="F.A.Q" url="/faqs" />
- </div>
-
- <p className="font-medium mb-2">Pengaturan Akun</p>
- <div className="flex flex-col mb-6">
- <Menu icon={<MapIcon className="w-5" />} name="Daftar Alamat" url="/my/address" />
- <Menu icon={<ArrowRightOnRectangleIcon className="w-5" />} name="Keluar Akun" url="/logout" />
- </div>
- </div>
- </Layout>
- </WithAuth>
- );
-} \ No newline at end of file
diff --git a/src/pages/my/menu.jsx b/src/pages/my/menu.jsx
new file mode 100644
index 00000000..b9fd30ee
--- /dev/null
+++ b/src/pages/my/menu.jsx
@@ -0,0 +1,100 @@
+import Divider from '@/core/components/elements/Divider/Divider'
+import Link from '@/core/components/elements/Link/Link'
+import AppLayout from '@/core/components/layouts/AppLayout'
+import useAuth from '@/core/hooks/useAuth'
+import { deleteAuth } from '@/core/utils/auth'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import { ChevronRightIcon, UserIcon } from '@heroicons/react/24/solid'
+import { useRouter } from 'next/router'
+
+export default function Menu() {
+ const auth = useAuth()
+ const router = useRouter()
+
+ const logout = () => {
+ deleteAuth()
+ router.push('/login')
+ }
+
+ return (
+ <IsAuth>
+ <AppLayout title='Menu Utama'>
+ <Link
+ href='/my/profile'
+ className='p-4 flex items-center'
+ >
+ <div className='rounded-full p-3 bg-gray_r-6 text-gray_r-12/80'>
+ <UserIcon className='w-5' />
+ </div>
+ <div className='ml-4'>
+ <div className='font-semibold text-gray_r-12'>{auth?.name}</div>
+ {auth?.company && <div className='badge-solid-red mt-1'>Akun Bisnis</div>}
+ {!auth?.company && <div className='badge-gray mt-1'>Akun Individu</div>}
+ </div>
+ <div className='ml-auto !text-gray_r-12'>
+ <ChevronRightIcon className='w-6' />
+ </div>
+ </Link>
+
+ <Divider />
+
+ <div className='flex flex-col gap-y-6 py-6'>
+ <div>
+ <MenuHeader>Aktivitas Pembelian</MenuHeader>
+
+ <div className='divide-y divide-gray_r-6 border-y border-gray_r-6 mt-4'>
+ <LinkItem href='/my/transactions'>Daftar Transaksi</LinkItem>
+ <LinkItem href='/my/invoices'>Invoice & Faktur Pajak</LinkItem>
+ <LinkItem href='/my/wishlist'>Wishlist</LinkItem>
+ </div>
+ </div>
+
+ <div>
+ <MenuHeader>Pusat Bantuan</MenuHeader>
+
+ <div className='divide-y divide-gray_r-6 border-y border-gray_r-6 mt-4'>
+ <LinkItem href='/'>Customer Support</LinkItem>
+ <LinkItem href='/faqs'>F.A.Q</LinkItem>
+ </div>
+ </div>
+
+ <div>
+ <MenuHeader>Pengaturan Akun</MenuHeader>
+
+ <div className='divide-y divide-gray_r-6 border-y border-gray_r-6 mt-4'>
+ <LinkItem href='/my/address'>Daftar Alamat</LinkItem>
+ </div>
+
+ <div
+ onClick={logout}
+ className='p-4 mt-2'
+ >
+ <button className='w-full btn-red'>Keluar Akun</button>
+ </div>
+ </div>
+ </div>
+ </AppLayout>
+ </IsAuth>
+ )
+}
+
+const MenuHeader = ({ children, ...props }) => (
+ <div
+ {...props}
+ className='font-medium px-4 flex'
+ >
+ {children}
+ </div>
+)
+
+const LinkItem = ({ children, ...props }) => (
+ <Link
+ {...props}
+ className='!text-gray_r-12/70 !font-normal p-4 flex items-center'
+ >
+ {children}
+ <div className='ml-auto !text-gray_r-11'>
+ <ChevronRightIcon className='w-5' />
+ </div>
+ </Link>
+)
diff --git a/src/pages/my/profile.js b/src/pages/my/profile.js
deleted file mode 100644
index 97891259..00000000
--- a/src/pages/my/profile.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import { useState } from "react";
-import { toast } from "react-hot-toast";
-import AppBar from "@/components/layouts/AppBar";
-import Layout from "@/components/layouts/Layout";
-import WithAuth from "@/components/auth/WithAuth";
-import apiOdoo from "@/core/utils/apiOdoo";
-import {
- useAuth,
- setAuth as setAuthCookie,
- getAuth
-} from "@/core/utils/auth";
-
-export default function MyProfile() {
- const [auth, setAuth] = useAuth();
- const [editMode, setEditMode] = useState(false);
- const [password, setPassword] = useState('');
-
- const update = async (e) => {
- e.preventDefault();
- let dataToUpdate = {
- name: auth.name,
- phone: auth.phone,
- mobile: auth.mobile
- };
- if (password) dataToUpdate.password = password;
- let update = await apiOdoo('PUT', `/api/v1/user/${auth.id}`, dataToUpdate);
- setAuthCookie(update.user);
- cancelEdit();
- toast.success('Berhasil mengubah profil', { duration: 1500 });
- };
-
- const handleInput = (e) => {
- let authToUpdate = auth;
- authToUpdate[e.target.name] = e.target.value;
- setAuth({ ...authToUpdate });
- };
-
- const cancelEdit = () => {
- setEditMode(false);
- setAuth(getAuth());
- setPassword('');
- }
-
- return (
- <WithAuth>
- <Layout>
- <AppBar title="Akun Saya" />
-
- <form onSubmit={update} className="w-full px-4">
- { auth && (
- <>
- <label className="form-label mt-4 mb-2">Email</label>
- <input
- type="text"
- className="form-input"
- placeholder="johndoe@gmail.com"
- name="email"
- value={auth.email}
- onChange={handleInput}
- disabled={true}
- />
-
- <label className="form-label mt-4 mb-2">Nama Lengkap</label>
- <input
- type="text"
- className="form-input"
- placeholder="John Doe"
- name="name"
- value={auth.name}
- onChange={handleInput}
- disabled={!editMode}
- />
-
- <label className="form-label mt-4 mb-2">No Telepon</label>
- <input
- type="tel"
- className="form-input"
- placeholder="08xxxxxxxx"
- name="phone"
- value={auth.phone}
- onChange={handleInput}
- disabled={!editMode}
- />
-
- <label className="form-label mt-4 mb-2">No Handphone</label>
- <input
- type="tel"
- className="form-input"
- placeholder="08xxxxxxxx"
- name="mobile"
- value={auth.mobile}
- onChange={handleInput}
- disabled={!editMode}
- />
-
- <label className="form-label mt-4 mb-2">Kata Sandi</label>
- <input
- type="password"
- className="form-input"
- placeholder="••••••••"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- disabled={!editMode}
- />
- </>
- ) }
-
- { editMode && (
- <div className="flex gap-x-3 mt-6">
- <button
- type="button"
- className="btn-light flex-1 float-right"
- onClick={cancelEdit}
- >
- Batal
- </button>
- <button type="submit" className="btn-yellow flex-1 float-right">Simpan</button>
- </div>
- ) }
-
- { !editMode && (
- <button
- type="button"
- className="btn-light float-right mt-6 w-full"
- onClick={() => setEditMode(true)}
- >
- Ubah Profil
- </button>
- ) }
- </form>
- </Layout>
- </WithAuth>
- );
-} \ No newline at end of file
diff --git a/src/pages/my/profile.jsx b/src/pages/my/profile.jsx
new file mode 100644
index 00000000..72a1ee3c
--- /dev/null
+++ b/src/pages/my/profile.jsx
@@ -0,0 +1,19 @@
+import Divider from '@/core/components/elements/Divider/Divider'
+import AppLayout from '@/core/components/layouts/AppLayout'
+import useAuth from '@/core/hooks/useAuth'
+import CompanyProfile from '@/lib/auth/components/CompanyProfile'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import PersonalProfile from '@/lib/auth/components/PersonalProfile'
+
+export default function Profile() {
+ const auth = useAuth()
+ return (
+ <IsAuth>
+ <AppLayout title='Akun Saya'>
+ <PersonalProfile />
+ <Divider />
+ {auth?.parentId && <CompanyProfile />}
+ </AppLayout>
+ </IsAuth>
+ )
+}
diff --git a/src/pages/my/transaction/[id].js b/src/pages/my/transaction/[id].js
deleted file mode 100644
index fb806aa4..00000000
--- a/src/pages/my/transaction/[id].js
+++ /dev/null
@@ -1,265 +0,0 @@
-import AppBar from "@/components/layouts/AppBar";
-import Layout from "@/components/layouts/Layout";
-import LineDivider from "@/components/elements/LineDivider";
-import WithAuth from "@/components/auth/WithAuth";
-import { useCallback, useEffect, useRef, useState } from "react";
-import apiOdoo from "@/core/utils/apiOdoo";
-import { useRouter } from "next/router";
-import { useAuth } from "@/core/utils/auth";
-import currencyFormat from "@/core/utils/currencyFormat";
-import DescriptionRow from "@/components/elements/DescriptionRow";
-import { TransactionDetailAddress } from "@/components/transactions/TransactionDetail";
-import { SkeletonList } from "@/components/elements/Skeleton";
-import Link from "@/components/elements/Link";
-import { ChevronRightIcon } from "@heroicons/react/24/outline";
-import Alert from "@/components/elements/Alert";
-import TransactionStatusBadge from "@/components/transactions/TransactionStatusBadge";
-import useConfirmAlert from "@/lib/elements/hooks/useConfirmAlert";
-import { toast } from "react-hot-toast";
-import useBottomPopup from "@/lib/elements/hooks/useBottomPopup";
-import getFileBase64 from "@/core/utils/getFileBase64";
-import VariantGroupCard from "@/components/variants/VariantGroupCard";
-
-export default function DetailTransaction() {
- const router = useRouter();
- const { id } = router.query;
- const [ auth ] = useAuth();
- const [ transaction, setTransaction ] = useState(null);
-
- const loadTransaction = useCallback(async () => {
- if (auth && id) {
- const dataTransaction = await apiOdoo('GET', `/api/v1/partner/${auth?.partner_id}/sale_order/${id}`);
- setTransaction(dataTransaction);
- }
- }, [ auth, id ]);
-
- useEffect(() => {
- loadTransaction();
- }, [ loadTransaction ]);
-
- const submitCancelTransaction = async (data) => {
- const isCancelled = await apiOdoo('POST', `/api/v1/partner/${auth.partner_id}/sale_order/${data.id}/cancel`);
- if (isCancelled) {
- toast.success('Berhasil batalkan transaksi');
- loadTransaction();
- }
- }
-
- const {
- openConfirmAlert,
- ConfirmAlert
- } = useConfirmAlert({
- title: 'Batalkan Transaksi',
- caption: 'Apakah anda yakin untuk membatalkan transaksi?',
- closeText: 'Tidak',
- submitText: 'Iya, batalkan',
- onSubmit: submitCancelTransaction
- });
-
- const UploadPurchaseOrder = () => {
- const nameRef = useRef('');
- const fileRef = useRef('');
-
- const submitUploadPurchaseOrder = async (e) => {
- e.preventDefault();
- const file = fileRef.current.files[0];
- const name = nameRef.current.value;
- if (file.size > 5000000) {
- toast.error('Maksimal ukuran file adalah 5MB', {
- position: 'bottom-center'
- });
- return;
- }
- const parameter = {
- name,
- file: await getFileBase64(file)
- };
- const isUploaded = await apiOdoo('POST', `/api/v1/partner/${auth.partner_id}/sale_order/${transaction.id}/upload_po`, parameter);
- if (isUploaded) {
- toast.success('Berhasil upload PO');
- loadTransaction();
- closePopup();
- }
- };
-
- return (
- <form className="flex flex-col gap-y-4" onSubmit={submitUploadPurchaseOrder}>
- <div>
- <label className="form-label mb-2">Nama PO</label>
- <input className="form-input" type="text" ref={nameRef} required />
- </div>
- <div>
- <label className="form-label mb-2">Dokumen PO</label>
- <input className="form-input" type="file" ref={fileRef} required />
- </div>
- <button type="submit" className="btn-yellow w-full mt-2">Upload</button>
- </form>
- );
- }
-
- const {
- closePopup,
- BottomPopup,
- openPopup
- } = useBottomPopup({
- title: 'Upload PO',
- children: UploadPurchaseOrder
- });
-
- const downloadPurchaseOrder = () => {
- const url = `${process.env.ODOO_HOST}/api/v1/partner/${auth.partner_id}/sale_order/${transaction.id}/download_po/${transaction.token}`;
- window.open(url, 'download')
- };
-
- const downloadQuotation = () => {
- const url = `${process.env.ODOO_HOST}/api/v1/partner/${auth.partner_id}/sale_order/${transaction.id}/download/${transaction.token}`;
- window.open(url, 'download')
- };
-
- const checkout = async () => {
- if (!transaction.purchase_order_file) {
- toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan')
- return
- }
- await apiOdoo('POST', `/api/v1/partner/${auth?.partner_id}/sale_order/${id}/checkout`)
- toast.success('Berhasil melanjutkan pesanan')
- loadTransaction()
- }
-
- return (
- <WithAuth>
- <Layout className="pb-4">
- <AppBar title="Detail Transaksi" />
-
- { transaction ? (
- <>
- <div className="p-4 flex flex-col gap-y-4">
- <DescriptionRow label="Status Transaksi">
- <div className="flex justify-end">
- <TransactionStatusBadge status={transaction?.status} />
- </div>
- </DescriptionRow>
- <DescriptionRow label="No Transaksi">
- { transaction?.name }
- </DescriptionRow>
- <DescriptionRow label="Ketentuan Pembayaran">
- { transaction?.payment_term }
- </DescriptionRow>
- <DescriptionRow label="Nama Sales">
- { transaction?.sales }
- </DescriptionRow>
- <DescriptionRow label="Waktu Transaksi">
- { transaction?.date_order }
- </DescriptionRow>
- </div>
-
- <LineDivider />
-
- <div className="p-4 flex flex-col gap-y-4">
- <DescriptionRow label="Purchase Order">
- { transaction?.purchase_order_name || '-' }
- </DescriptionRow>
- <div className="flex items-center">
- <p className="text-gray_r-11 leading-none">Dokumen PO</p>
- <button
- type="button"
- className="btn-light py-1.5 px-3 ml-auto"
- onClick={transaction?.purchase_order_file ? downloadPurchaseOrder : openPopup}
- >
- { transaction?.purchase_order_file ? 'Download' : 'Upload' }
- </button>
- </div>
- </div>
-
- <LineDivider />
-
- <p className="h2 p-4">Detail Produk</p>
-
- <div className="mt-2 p-4 pt-0 flex flex-col gap-y-3">
- <VariantGroupCard
- variants={transaction?.products}
- buyMore
- />
- <div className="flex justify-between mt-3 font-medium">
- <p>Total Belanja</p>
- <p>{ currencyFormat(transaction?.amount_total || 0) }</p>
- </div>
- </div>
-
- <LineDivider />
-
- <TransactionDetailAddress transaction={transaction} />
-
- <LineDivider />
-
- <div className="p-4">
- <p className="h2">Invoice</p>
- <div className="flex flex-col gap-y-3 mt-4">
- { transaction?.invoices?.map((invoice, index) => (
- <Link href={`/my/invoice/${invoice.id}`} key={index}>
- <div className="shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between">
- <div>
- <p className="mb-2">{ invoice?.name }</p>
- <div className="flex items-center gap-x-1">
- { invoice.amount_residual > 0 ? (
- <div className="badge-red">Belum Lunas</div>
- ) : (
- <div className="badge-green">Lunas</div>
- ) }
- <p className="text-caption-2 text-gray_r-11">
- { currencyFormat(invoice.amount_total) }
- </p>
- </div>
- </div>
- <ChevronRightIcon className="w-5 stroke-2" />
- </div>
- </Link>
- )) }
- { transaction?.invoices?.length === 0 && (
- <Alert type='info' className='text-center'>
- Belum ada Invoice
- </Alert>
- ) }
- </div>
- </div>
-
- <LineDivider />
-
- <div className="px-4">
- { transaction?.status == 'draft' && (
- <button
- className="btn-yellow w-full mt-4"
- onClick={checkout}
- >
- Lanjutkan Transaksi
- </button>
- ) }
- <button
- className="btn-light w-full mt-4"
- disabled={transaction?.status != 'draft'}
- onClick={downloadQuotation}
- >
- Download Quotation
- </button>
- { transaction?.status != 'draft' && (
- <button
- className="btn-light w-full mt-4"
- disabled={transaction?.status != 'waiting'}
- onClick={() => openConfirmAlert(transaction)}
- >
- Batalkan Transaksi
- </button>
- ) }
- </div>
- </>
- ) : (
- <div className="p-4 py-6">
- <SkeletonList number={12} />
- </div>
- ) }
- { ConfirmAlert }
- { BottomPopup }
- </Layout>
- </WithAuth>
- );
-} \ No newline at end of file
diff --git a/src/pages/my/transaction/[id].jsx b/src/pages/my/transaction/[id].jsx
new file mode 100644
index 00000000..5167748c
--- /dev/null
+++ b/src/pages/my/transaction/[id].jsx
@@ -0,0 +1,16 @@
+import AppLayout from '@/core/components/layouts/AppLayout'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import TransactionComponent from '@/lib/transaction/components/Transaction'
+import { useRouter } from 'next/router'
+
+export default function Transaction() {
+ const router = useRouter()
+
+ return (
+ <IsAuth>
+ <AppLayout title='Transaksi'>
+ <TransactionComponent id={router.query.id} />
+ </AppLayout>
+ </IsAuth>
+ )
+}
diff --git a/src/pages/my/transactions.js b/src/pages/my/transactions.js
deleted file mode 100644
index 8be43af7..00000000
--- a/src/pages/my/transactions.js
+++ /dev/null
@@ -1,198 +0,0 @@
-import { useRouter } from "next/router";
-import AppBar from "@/components/layouts/AppBar";
-import Layout from "@/components/layouts/Layout";
-import WithAuth from "@/components/auth/WithAuth";
-import { useCallback, useEffect, useRef, useState } from "react";
-import { useAuth } from "@/core/utils/auth";
-import apiOdoo from "@/core/utils/apiOdoo";
-import currencyFormat from "@/core/utils/currencyFormat";
-import { EllipsisVerticalIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
-import Link from "@/components/elements/Link";
-import Pagination from "@/components/elements/Pagination";
-import Alert from "@/components/elements/Alert";
-import TransactionStatusBadge from "@/components/transactions/TransactionStatusBadge";
-import { toast } from "react-hot-toast";
-import useConfirmAlert from "@/lib/elements/hooks/useConfirmAlert";
-import useBottomPopup from "@/lib/elements/hooks/useBottomPopup";
-
-export default function Transactions() {
- const [ auth ] = useAuth();
- const router = useRouter();
- const {
- q,
- page = 1
- } = router.query;
-
- const [ transactions, setTransactions ] = useState([]);
-
- const [ pageCount, setPageCount ] = useState(0);
- const [ isLoading, setIsLoading ] = useState(true);
-
- const searchQueryRef = useRef();
- const loadTransactions = useCallback(async () => {
- if (auth) {
- const limit = 10;
- let offset = (page - 1) * 10;
- let queryParams = [`limit=${limit}`, `offset=${offset}`];
- if (q) queryParams.push(`name=${q}`);
- queryParams = queryParams.join('&');
- queryParams = queryParams ? '?' + queryParams : '';
-
- const dataTransactions = await apiOdoo('GET', `/api/v1/partner/${auth.partner_id}/sale_order${queryParams}`);
- setTransactions(dataTransactions);
- setPageCount(Math.ceil(dataTransactions?.sale_order_total / limit));
- setIsLoading(false);
- };
- }, [ auth, q, page ]);
-
- useEffect(() => {
- loadTransactions();
- }, [ loadTransactions ]);
-
- const actionSearch = (e) => {
- e.preventDefault();
- let queryParams = [];
- if (searchQueryRef.current.value) queryParams.push(`q=${searchQueryRef.current.value}`);
- queryParams = queryParams.join('&');
- queryParams = queryParams ? `?${queryParams}` : '';
- router.push(`/my/transactions${queryParams}`);
- };
-
- const downloadPurchaseOrder = (data) => {
- const url = `${process.env.ODOO_HOST}/api/v1/partner/${auth.partner_id}/sale_order/${data.id}/download_po/${data.token}`;
- window.open(url, 'download');
- closePopup();
- };
-
- const downloadQuotation = (data) => {
- const url = `${process.env.ODOO_HOST}/api/v1/partner/${auth.partner_id}/sale_order/${data.id}/download/${data.token}`;
- window.open(url, 'download');
- closePopup();
- };
-
- const childrenPopup = (data) => (
- <div className="flex flex-col gap-y-6">
- <button
- className="text-left disabled:opacity-60"
- disabled={!data?.purchase_order_file}
- onClick={() => downloadPurchaseOrder(data)}
- >
- Download PO
- </button>
- <button
- className="text-left disabled:opacity-60"
- disabled={data?.status != 'draft'}
- onClick={() => downloadQuotation(data)}
- >
- Download Quotation
- </button>
- <button
- className="text-left disabled:opacity-60"
- disabled={ data?.status != 'waiting' }
- onClick={() => {openConfirmAlert(data); closePopup()}}
- >
- Batalkan Transaksi
- </button>
- </div>
- );
-
- const {
- closePopup,
- openPopup,
- BottomPopup
- } = useBottomPopup({
- title: 'Lainnya',
- children: childrenPopup
- });
-
- const submitCancelTransaction = async (data) => {
- const isCancelled = await apiOdoo('POST', `/api/v1/partner/${auth.partner_id}/sale_order/${data.id}/cancel`);
- if (isCancelled) {
- toast.success('Berhasil batalkan transaksi');
- loadTransactions();
- }
- }
-
- const {
- openConfirmAlert,
- ConfirmAlert
- } = useConfirmAlert({
- title: 'Batalkan Transaksi',
- caption: 'Apakah anda yakin untuk membatalkan transaksi?',
- closeText: 'Tidak',
- submitText: 'Ya, Batalkan',
- onSubmit: submitCancelTransaction
- });
-
- return (
- <WithAuth>
- <Layout>
- <AppBar title="Transaksi" />
-
- <form onSubmit={actionSearch} className="p-4 pb-0 flex gap-x-4">
- <input
- type="text"
- className="form-input"
- placeholder="Cari Transaksi"
- ref={searchQueryRef}
- defaultValue={q}
- />
- <button type="submit" className="border border-gray_r-7 rounded px-3">
- <MagnifyingGlassIcon className="w-5"/>
- </button>
- </form>
-
- <div className="p-4 flex flex-col gap-y-5">
- { transactions?.sale_order_total === 0 && !isLoading && (
- <Alert type="info" className="text-center">
- Transaksi tidak ditemukan
- </Alert>
- ) }
- { transactions?.sale_orders?.map((transaction, index) => (
- <div className="p-4 shadow border border-gray_r-3 rounded-md" key={index}>
- <div className="grid grid-cols-2">
- <Link href={`/my/transaction/${transaction.id}`}>
- <span className="text-caption-2 text-gray_r-11">No. Transaksi</span>
- <h2 className="text-red_r-11 mt-1">{ transaction.name }</h2>
- </Link>
- <div className="flex gap-x-1 justify-end">
- <TransactionStatusBadge status={transaction.status} />
- <EllipsisVerticalIcon className="w-5 h-5" onClick={() => openPopup(transaction)} />
- </div>
- </div>
- <Link href={`/my/transaction/${transaction.id}`}>
- <div className="grid grid-cols-2 mt-3">
- <div>
- <span className="text-caption-2 text-gray_r-11">No. Purchase Order</span>
- <p className="mt-1 font-medium text-gray_r-12">{ transaction.purchase_order_name || '-' }</p>
- </div>
- <div className="text-right">
- <span className="text-caption-2 text-gray_r-11">Total Invoice</span>
- <p className="mt-1 font-medium text-gray_r-12">{ transaction.invoice_count } Invoice</p>
- </div>
- </div>
- <div className="grid grid-cols-2 mt-3">
- <div>
- <span className="text-caption-2 text-gray_r-11">Sales</span>
- <p className="mt-1 font-medium text-gray_r-12">{ transaction.sales }</p>
- </div>
- <div className="text-right">
- <span className="text-caption-2 text-gray_r-11">Total Harga</span>
- <p className="mt-1 font-medium text-gray_r-12">{ currencyFormat(transaction.amount_total) }</p>
- </div>
- </div>
- </Link>
- </div>
- )) }
- </div>
-
- <div className="pb-6 pt-2">
- <Pagination currentPage={page} pageCount={pageCount} url={`/my/transactions${q ? `?q=${q}` : ''}`} />
- </div>
-
- { ConfirmAlert }
- { BottomPopup }
- </Layout>
- </WithAuth>
- );
-}; \ No newline at end of file
diff --git a/src/pages/my/transactions.jsx b/src/pages/my/transactions.jsx
new file mode 100644
index 00000000..30b9be07
--- /dev/null
+++ b/src/pages/my/transactions.jsx
@@ -0,0 +1,15 @@
+import AppLayout from '@/core/components/layouts/AppLayout'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import dynamic from 'next/dynamic'
+
+const TransactionsComponent = dynamic(() => import('@/lib/transaction/components/Transactions'))
+
+export default function Transactions() {
+ return (
+ <IsAuth>
+ <AppLayout title='Transaksi'>
+ <TransactionsComponent />
+ </AppLayout>
+ </IsAuth>
+ )
+}
diff --git a/src/pages/my/wishlist.js b/src/pages/my/wishlist.js
deleted file mode 100644
index 3d479802..00000000
--- a/src/pages/my/wishlist.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import WithAuth from "@/components/auth/WithAuth";
-import Alert from "@/components/elements/Alert";
-import Pagination from "@/components/elements/Pagination";
-import Spinner from "@/components/elements/Spinner";
-import AppBar from "@/components/layouts/AppBar";
-import Layout from "@/components/layouts/Layout";
-import ProductCard from "@/components/products/ProductCard";
-import apiOdoo from "@/core/utils/apiOdoo";
-import { useAuth } from "@/core/utils/auth";
-import { useRouter } from "next/router";
-import { useEffect, useState } from "react";
-
-export default function Wishlist() {
- const [ auth ] = useAuth();
- const router = useRouter();
- const { page = 1 } = router.query;
- const [ wishlists, setWishlists ] = useState(null);
- const [ pageCount, setPageCount ] = useState(0);
-
- useEffect(() => {
- const loadWishlist = async () => {
- const limit = 10;
- const offset = (page - 1) * limit;
- if (auth) {
- const dataWishlist = await apiOdoo('GET', `/api/v1/user/${auth.id}/wishlist?limit=${limit}&offset=${offset}`);
- setWishlists(dataWishlist);
- setPageCount(Math.ceil(dataWishlist.product_total / limit));
- }
- }
- loadWishlist();
- }, [ auth, page ]);
-
- return (
- <WithAuth>
- <Layout>
- <AppBar title='Wishlist' />
-
- <div className="px-4 py-6">
- { !wishlists && (
- <Spinner className="w-6 h-6 text-gray-600 fill-gray-900 mx-auto" />
- ) }
- { wishlists?.products?.length == 0 && (
- <Alert type='info' className='text-center'>
- Wishlist anda masih kosong
- </Alert>
- ) }
- <div className="grid grid-cols-2 gap-3">
- {wishlists?.products.map((product) => (
- <ProductCard key={product.id} data={product} />
- ))}
- </div>
-
- <div className="mt-6">
- <Pagination currentPage={page} pageCount={pageCount} url={`/my/wishlist`} />
- </div>
- </div>
- </Layout>
- </WithAuth>
- )
-} \ No newline at end of file
diff --git a/src/pages/my/wishlist.jsx b/src/pages/my/wishlist.jsx
new file mode 100644
index 00000000..196adf50
--- /dev/null
+++ b/src/pages/my/wishlist.jsx
@@ -0,0 +1,13 @@
+import AppLayout from '@/core/components/layouts/AppLayout'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import Wishlists from '@/lib/wishlist/components/Wishlists'
+
+export default function Wishlist() {
+ return (
+ <IsAuth>
+ <AppLayout title='Wishlist'>
+ <Wishlists />
+ </AppLayout>
+ </IsAuth>
+ )
+}
diff --git a/src/pages/register.js b/src/pages/register.js
deleted file mode 100644
index 39bd137f..00000000
--- a/src/pages/register.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import axios from "axios";
-import Head from "next/head";
-import Image from "next/image";
-import Link from "@/components/elements/Link";
-import { useEffect, useState } from "react";
-import Alert from "@/components/elements/Alert";
-import Layout from "@/components/layouts/Layout";
-import Spinner from "@/components/elements/Spinner";
-import Logo from "@/images/logo.png";
-
-export default function Login() {
- const [email, setEmail] = useState('');
- const [name, setName] = useState('');
- const [password, setPassword] = useState('');
- const [isInputFulfilled, setIsInputFulfilled] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [alert, setAlert] = useState();
-
- useEffect(() => {
- setIsInputFulfilled(email && name && password);
- }, [email, name, password]);
-
- const register = async (e) => {
- e.preventDefault();
- setIsLoading(true);
- let register = await axios.post(`${process.env.SELF_HOST}/api/register`, {email, name, password});
- if (register.data.register) {
- await axios.post(`${process.env.SELF_HOST}/api/activation-request`, {email});
- setAlert({
- component: <>Berhasil mendaftarkan akun anda, cek email untuk melakukan aktivasi akun</>,
- type: 'success'
- });
- setEmail('');
- setName('');
- setPassword('');
- } else {
- switch (register.data.reason) {
- case 'EMAIL_USED':
- setAlert({
- component: <>Email telah digunakan</>,
- type: 'info'
- });
- break;
- }
- }
- setIsLoading(false);
- }
-
- return (
- <>
- <Head>
- <title>Daftar - Indoteknik</title>
- </Head>
- <Layout className="max-w-lg mx-auto flex flex-col items-center px-4 pb-8">
- <Link href="/" className="mt-16">
- <Image src={Logo} alt="Logo Indoteknik" width={165} height={42} />
- </Link>
- <h1 className="text-2xl mt-4 text-center">Mudahkan Pembelian dengan Indoteknik</h1>
- <h2 className="text-gray_r-11 font-normal mt-2 mb-4">Daftar untuk melanjutkan belanja</h2>
- {alert ? (
- <Alert className="text-center" type={alert.type}>{alert.component}</Alert>
- ) : ''}
- <form onSubmit={register} className="w-full">
- <label className="form-label mt-4 mb-2">Alamat Email</label>
- <input
- type="email"
- className="form-input bg-gray_r-2"
- placeholder="johndoe@gmail.com"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- />
- <label className="form-label mt-4 mb-2">Nama Lengkap</label>
- <input
- type="text"
- className="form-input bg-gray_r-2"
- placeholder="John Doe"
- value={name}
- onChange={(e) => setName(e.target.value)}
- />
- <label className="form-label mt-4 mb-2">Kata Sandi</label>
- <input
- type="password"
- className="form-input bg-gray_r-2"
- placeholder="••••••••"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- />
- <button type="submit" disabled={!isInputFulfilled} className="btn-yellow font-semibold mt-4 w-full">
- {isLoading ? (
- <div className="flex justify-center items-center gap-x-2">
- <Spinner className="w-4 h-4 text-gray-600 fill-gray-900" /> <span>Loading...</span>
- </div>
- ) : 'Daftar'}
- </button>
- </form>
- <p className="text-gray-700 mt-4">Sudah punya akun Indoteknik? <Link href="/login">Masuk</Link></p>
- </Layout>
- </>
- )
-} \ No newline at end of file
diff --git a/src/pages/register.jsx b/src/pages/register.jsx
new file mode 100644
index 00000000..0ca1e81e
--- /dev/null
+++ b/src/pages/register.jsx
@@ -0,0 +1,11 @@
+import SimpleFooter from '@/core/components/elements/Footer/SimpleFooter'
+import RegisterComponent from '@/lib/auth/components/Register'
+
+export default function Register() {
+ return (
+ <>
+ <RegisterComponent />
+ <SimpleFooter />
+ </>
+ )
+}
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&nbsp;
- {pageCount > 1 ? (
- <>
- {productStart + 1}-{
- (productStart + productRows) > productFound ? productFound : productStart + productRows
- }
- &nbsp;dari&nbsp;
- </>
- ) : ''}
- {searchResults.response.numFound}
- &nbsp;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..33f81fa2
--- /dev/null
+++ b/src/pages/shop/brands/[slug].jsx
@@ -0,0 +1,25 @@
+import dynamic from 'next/dynamic'
+import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'
+import { useRouter } from 'next/router'
+import _ from 'lodash'
+
+const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
+const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch'))
+const Brand = dynamic(() => import('@/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>
+ )
+}
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/brands/index.jsx b/src/pages/shop/brands/index.jsx
new file mode 100644
index 00000000..5c01fd19
--- /dev/null
+++ b/src/pages/shop/brands/index.jsx
@@ -0,0 +1,10 @@
+import BasicLayout from '@/core/components/layouts/BasicLayout'
+import BrandsComponent from '@/lib/brand/components/Brands'
+
+export default function Brands() {
+ return (
+ <BasicLayout>
+ <BrandsComponent />
+ </BasicLayout>
+ )
+}
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..97f98843
--- /dev/null
+++ b/src/pages/shop/cart.jsx
@@ -0,0 +1,12 @@
+import dynamic from 'next/dynamic'
+
+const AppLayout = dynamic(() => import('@/core/components/layouts/AppLayout'))
+const CartComponent = dynamic(() => import('@/lib/cart/components/Cart'))
+
+export default function Cart() {
+ return (
+ <AppLayout title='Keranjang'>
+ <CartComponent />
+ </AppLayout>
+ )
+}
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/finish.jsx b/src/pages/shop/checkout/finish.jsx
new file mode 100644
index 00000000..cc64199f
--- /dev/null
+++ b/src/pages/shop/checkout/finish.jsx
@@ -0,0 +1,16 @@
+import BasicLayout from '@/core/components/layouts/BasicLayout'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import FinishCheckoutComponent from '@/lib/checkout/components/FinishCheckout'
+import { useRouter } from 'next/router'
+
+export default function Finish() {
+ const router = useRouter()
+
+ return (
+ <IsAuth>
+ <BasicLayout>
+ <FinishCheckoutComponent id={router.query.id || 0} />
+ </BasicLayout>
+ </IsAuth>
+ )
+}
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/checkout/index.jsx b/src/pages/shop/checkout/index.jsx
new file mode 100644
index 00000000..015a11b3
--- /dev/null
+++ b/src/pages/shop/checkout/index.jsx
@@ -0,0 +1,13 @@
+import AppLayout from '@/core/components/layouts/AppLayout'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import CheckoutComponent from '@/lib/checkout/components/Checkout'
+
+export default function Checkout() {
+ return (
+ <IsAuth>
+ <AppLayout title='Checkout'>
+ <CheckoutComponent />
+ </AppLayout>
+ </IsAuth>
+ )
+}
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..cc6924a3
--- /dev/null
+++ b/src/pages/shop/product/[slug].jsx
@@ -0,0 +1,31 @@
+import Seo from '@/core/components/Seo'
+import { getIdFromSlug } from '@/core/utils/slug'
+import productApi from '@/lib/product/api/productApi'
+import dynamic from 'next/dynamic'
+
+const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
+const Product = dynamic(() => import('@/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>
+ )
+}
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/finish.jsx b/src/pages/shop/quotation/finish.jsx
new file mode 100644
index 00000000..15881ea0
--- /dev/null
+++ b/src/pages/shop/quotation/finish.jsx
@@ -0,0 +1,44 @@
+import Link from '@/core/components/elements/Link/Link'
+import BasicLayout from '@/core/components/layouts/BasicLayout'
+import useAuth from '@/core/hooks/useAuth'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import { EnvelopeIcon } from '@heroicons/react/24/outline'
+import { useRouter } from 'next/router'
+
+export default function FinishQuotation() {
+ const auth = useAuth()
+ const router = useRouter()
+ const { id } = router.query
+ return (
+ <IsAuth>
+ <BasicLayout>
+ <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>
+ </BasicLayout>
+ </IsAuth>
+ )
+}
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/quotation/index.jsx b/src/pages/shop/quotation/index.jsx
new file mode 100644
index 00000000..ff8b8644
--- /dev/null
+++ b/src/pages/shop/quotation/index.jsx
@@ -0,0 +1,13 @@
+import AppLayout from '@/core/components/layouts/AppLayout'
+import IsAuth from '@/lib/auth/components/IsAuth'
+import QuotationComponent from '@/lib/quotation/components/Quotation'
+
+export default function Quotation() {
+ return (
+ <IsAuth>
+ <AppLayout title='Quotation'>
+ <QuotationComponent />
+ </AppLayout>
+ </IsAuth>
+ )
+}
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&nbsp;
- {pageCount > 1 ? (
- <>
- {productStart + 1}-{
- (productStart + productRows) > productFound ? productFound : productStart + productRows
- }
- &nbsp;dari&nbsp;
- </>
- ) : ''}
- {searchResults.response.numFound}
- &nbsp;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..345b715a
--- /dev/null
+++ b/src/pages/shop/search.jsx
@@ -0,0 +1,21 @@
+import dynamic from 'next/dynamic'
+import { useRouter } from 'next/router'
+import _ from 'lodash-contrib'
+
+const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
+const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch'))
+
+export default function Search() {
+ const router = useRouter()
+
+ return (
+ <BasicLayout>
+ {!_.isEmpty(router.query) && (
+ <ProductSearch
+ query={router.query}
+ prefixUrl='/shop/search'
+ />
+ )}
+ </BasicLayout>
+ )
+}