From 8ee5432961a5b73e8e5c42af2eda05621723c9e7 Mon Sep 17 00:00:00 2001 From: IT Fixcomart Date: Fri, 11 Nov 2022 18:02:11 +0700 Subject: Connect to solr (search product), header component with title, fix product card layout, show product search result --- src/components/Header.js | 32 ++++- src/components/ProductCard.js | 18 ++- src/helpers/apiOdoo.js | 4 +- src/helpers/slug.js | 2 +- src/images/logo.png | Bin 0 -> 49879 bytes src/images/page-not-found.png | Bin 0 -> 42280 bytes src/pages/404.js | 24 ++++ src/pages/api/shop/search.js | 71 ++++++++++ src/pages/index.js | 2 +- src/pages/shop/product/[slug].js | 202 ++++++++++++---------------- src/pages/shop/search.js | 46 +++++++ src/styles/globals.css | 279 ++++++++++++++++++++++----------------- 12 files changed, 429 insertions(+), 251 deletions(-) create mode 100644 src/images/logo.png create mode 100644 src/images/page-not-found.png create mode 100644 src/pages/404.js create mode 100644 src/pages/api/shop/search.js create mode 100644 src/pages/shop/search.js (limited to 'src') diff --git a/src/components/Header.js b/src/components/Header.js index 3814ed20..f7607a18 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -6,18 +6,29 @@ import MenuIcon from "../icons/menu.svg"; import ChevronRightIcon from "../icons/chevron-right.svg"; import { useState } from "react"; import Head from "next/head"; +import Logo from "../images/logo.png"; +import { useRouter } from "next/router"; -export default function Header() { +export default function Header({ title }) { + const router = useRouter(); + const { q = '' } = router.query; + const [searchQuery, setSearchQuery] = useState(q); const [isMenuActive, setIsMenuActive] = useState(false); const openMenu = () => setIsMenuActive(true); const closeMenu = () => setIsMenuActive(false); + const searchSubmit = (e) => { + e.preventDefault(); + router.push(`/shop/search?q=${searchQuery}`); + } + return ( <> + {title}
@@ -50,7 +61,7 @@ export default function Header() {
- Logo Indoteknik + Logo Indoteknik
@@ -61,9 +72,20 @@ export default function Header() {
-
- -
diff --git a/src/components/ProductCard.js b/src/components/ProductCard.js index 360e11ff..49bba235 100644 --- a/src/components/ProductCard.js +++ b/src/components/ProductCard.js @@ -24,19 +24,25 @@ export default function ProductCard({ data }) { {product.name}
-
+
{product.lowest_price.discount_percentage > 0 ? ( -
+
{product.lowest_price.discount_percentage}%

{currencyFormat(product.lowest_price.price)}

) : ''} -

{product.lowest_price.price_discount > 0 ? currencyFormat(product.lowest_price.price_discount) : 'Tanya harga'}

- {product.stock_total > 0 ? ( -
Ready Stock
+ {product.lowest_price.price_discount > 0 ? ( +

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

) : ( -
+ + Tanya Harga + )} + {product.stock_total > 0 ? ( +
Ready Stock
+ ) : ''}
diff --git a/src/helpers/apiOdoo.js b/src/helpers/apiOdoo.js index aff27bc3..b1e4ce6b 100644 --- a/src/helpers/apiOdoo.js +++ b/src/helpers/apiOdoo.js @@ -3,7 +3,7 @@ import { getCookie, setCookie } from 'cookies-next'; const axios = require('axios'); const renewToken = async () => { - let res = await axios.get(process.env.DB_HOST + '/api/token'); + let res = await axios.get(process.env.ODOO_HOST + '/api/token'); setCookie('token', res.data.result); return res.data.result; }; @@ -17,7 +17,7 @@ const getToken = async () => { const getOdoo = async (url) => { try { let token = await getToken(); - let res = await axios.get(process.env.DB_HOST + url, {headers: {Authorization: token}}); + let res = await axios.get(process.env.ODOO_HOST + url, {headers: {Authorization: token}}); if (res.data.status.code == 401) { await renewToken(); return getOdoo(url); diff --git a/src/helpers/slug.js b/src/helpers/slug.js index 5a8db08f..ed1a5b6a 100644 --- a/src/helpers/slug.js +++ b/src/helpers/slug.js @@ -1,5 +1,5 @@ const createSlug = (name, id) => { - return name.trim().replace(new RegExp(/[^A-Za-z0-9]/, 'g'), '-').toLowerCase() + '-' + id; + return name?.trim().replace(new RegExp(/[^A-Za-z0-9]/, 'g'), '-').toLowerCase() + '-' + id; } const getId = (slug) => { diff --git a/src/images/logo.png b/src/images/logo.png new file mode 100644 index 00000000..87c696aa Binary files /dev/null and b/src/images/logo.png differ diff --git a/src/images/page-not-found.png b/src/images/page-not-found.png new file mode 100644 index 00000000..296c0443 Binary files /dev/null and b/src/images/page-not-found.png differ diff --git a/src/pages/404.js b/src/pages/404.js new file mode 100644 index 00000000..83993f61 --- /dev/null +++ b/src/pages/404.js @@ -0,0 +1,24 @@ +import Image from "next/image"; +import Link from "next/link"; +import Header from "../components/Header"; +import PageNotFoundImage from "../images/page-not-found.png"; + +export default function PageNotFound() { + return ( + <> +
+
+ Halaman Tidak Ditemukan - Indoteknik +

Halaman tidak ditemukan

+
+ + Kembali ke beranda + + + Tanya admin + +
+
+ + ); +} \ No newline at end of file diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js new file mode 100644 index 00000000..3c3a1766 --- /dev/null +++ b/src/pages/api/shop/search.js @@ -0,0 +1,71 @@ +import axios from "axios"; + +const productResponseMap = (products) => { + return products.map((product) => { + let productMapped = { + id: product.product_id ? product.product_id[0] : '', + image: product.image ? product.image[0] : '', + code: product.default_code ? product.default_code[0] : '', + name: product.product_name ? product.product_name[0] : '', + lowest_price: { + 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, + }, + variant_total: product.variant_total ? product.variant_total[0] : 0, + stock_total: product.stock_total ? product.stock_total[0] : 0, + weight: product.weight ? product.weight[0] : 0, + manufacture: {}, + categories: [], + }; + + if (product.manufacture_id && product.brand) { + productMapped.manufacture = { + id: product.manufacture_id ? product.manufacture_id[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] : '', + } + ]; + + return productMapped; + }); +} + +export default async function handler(req, res) { + const { + q, + page = 1 + } = req.query; + + let limit = 30; + let offset = (page - 1) * limit; + + let parameter = [ + `facet.query=${q}`, + 'facet=true', + 'indent=true', + 'q.op=AND', + `q=${q}`, + 'facet.field=brand_str', + 'facet.field=category_name_str', + `start=${offset}`, + `rows=${limit}`, + ].join('&'); + + let result = await axios(process.env.SOLR_HOST + '/solr/products/select?' + parameter); + 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); + } catch (error) { + res.status(400).json({ error: error.message }); + } +} \ No newline at end of file diff --git a/src/pages/index.js b/src/pages/index.js index 40b36dc2..9d26d8ee 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -44,7 +44,7 @@ export default function Home() { return ( <> -
+
{ heroBanners?.map((banner, index) => ( diff --git a/src/pages/shop/product/[slug].js b/src/pages/shop/product/[slug].js index f50d5037..e601d8c0 100644 --- a/src/pages/shop/product/[slug].js +++ b/src/pages/shop/product/[slug].js @@ -1,17 +1,13 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import ProductCard from "../../../components/ProductCard"; import Header from "../../../components/Header"; import { getOdoo } from "../../../helpers/apiOdoo"; import { createSlug, getId } from "../../../helpers/slug"; import currencyFormat from "../../../helpers/currencyFormat"; -import Head from "next/head"; -import { Swiper, SwiperSlide } from "swiper/react"; import { LazyLoadImage } from "react-lazy-load-image-component"; -import ImagePlaceholderIcon from "../../../icons/image-placeholder.svg"; -import "swiper/css"; import "react-lazy-load-image-component/src/effects/blur.css"; +import ProductSlider from "../../../components/product/ProductSlider"; export async function getServerSideProps(context) { const { slug } = context.query; @@ -83,126 +79,98 @@ export default function ProductDetail({product}) { return ( <> - - {product.name + '- Indoteknik'} - -
-
- - - {product.manufacture.name || '-'} - -

{product.name}{activeVariant.attributes != '' ? ' - ' + activeVariant.attributes : ''}

- - {product.variant_total > 1 && selectedVariant == "" ? ( -

Harga mulai dari:

- ) : ''} - - {product.lowest_price.discount_percentage > 0 ? ( -
- {activeVariant.price.discount_percentage}% -

{currencyFormat(activeVariant.price.price)}

-
- ) : ''} - - {product.lowest_price.price > 0 ? ( -

{currencyFormat(activeVariant.price.price_discount)}

- ) : ( -

Dapatkan harga terbaik, hubungi kami.

- )} - -
-
- - -
-
- - setQuantity(e.target.value)} /> -
-
- -
- - -
+
+
+ + +
+ + {product.manufacture.name ?? '-'} + +

{product.name}{activeVariant.attributes ? ' - ' + activeVariant.attributes : ''}

+ + {product.variant_total > 1 && !selectedVariant ? ( +

Harga mulai dari:

+ ) : ''} -
-

Detail Produk

-
-

Jumlah Varian

-

{product.variant_total} Varian

-
-
-

Nomor SKU

-

SKU-{activeVariant.id}

+ {product.lowest_price.discount_percentage > 0 ? ( +
+ {activeVariant.price.discount_percentage}% +

{currencyFormat(activeVariant.price.price)}

+
+ ) : ''} + + {product.lowest_price.price > 0 ? ( +

{currencyFormat(activeVariant.price.price_discount)}

+ ) : ( +

Dapatkan harga terbaik, hubungi kami.

+ )} + +
+
+ + +
+
+ + setQuantity(e.target.value)} /> +
-
-

Part Number

-

{activeVariant.code}

+ +
+ +
-
-

Stok

-

- {activeVariant.stock > 0 ? (activeVariant.stock > 5 ? 'Lebih dari 5' : 'Kurang dari 5') : '0'} -

+ +
+

Detail Produk

+
+

Jumlah Varian

+

{product.variant_total} Varian

+
+
+

Nomor SKU

+

SKU-{activeVariant.id}

+
+
+

Part Number

+

{activeVariant.code}

+
+
+

Stok

+

+ {activeVariant.stock > 0 ? (activeVariant.stock > 5 ? 'Lebih dari 5' : 'Kurang dari 5') : '0'} +

+
+
+

Berat Barang

+

{activeVariant.weight > 0 ? activeVariant.weight : '1'} KG

+
-
-

Berat Barang

-

{activeVariant.weight > 0 ? activeVariant.weight : '1'} KG

+ +
+

Deskripsi Produk

+
/g, '') : 'Belum ada deskripsi produk.')}}>
-
-
-

Deskripsi Produk

-
/g, '') : 'Belum ada deskripsi produk.')}}>
-
+
+

Produk Lainnya

+ +
-
-

Produk Lainnya

- - {similarProducts?.products?.map((product, index) => ())} - - {similarProducts == null ? ( -
-
-
- -
-
-
-
-
-
- Loading... -
-
-
- -
-
-
-
-
-
- Loading... -
-
- ) : ''}
- -
+
); } \ No newline at end of file diff --git a/src/pages/shop/search.js b/src/pages/shop/search.js new file mode 100644 index 00000000..6e6839be --- /dev/null +++ b/src/pages/shop/search.js @@ -0,0 +1,46 @@ +import axios from "axios"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import Header from "../../components/Header"; +import ProductCard from "../../components/ProductCard"; + +export async function getServerSideProps(context) { + const { q, page = 1 } = context.query; + let searchResults = await axios(`http://${context.req.headers.host}/api/shop/search?q=${q}&page=${page}`); + searchResults = searchResults.data; + return { props: { searchResults, q } }; +} + +export default function ShopSearch({ searchResults, q, page = 1 }) { + const pageTotal = Math.ceil(searchResults.response.numFound / searchResults.responseHeader.params.rows); + + return ( + <> +
+
+
+

Produk

+
+ Menampilkan  + {searchResults.responseHeader.params.start + 1}-{searchResults.responseHeader.params.start + searchResults.responseHeader.params.rows} +  dari  + {searchResults.response.numFound} +  produk untuk pencarian {q} +
+
+ {searchResults.response.products.map((product) => ( + + ))} +
+
+ {[...Array(pageTotal)].map((v, i) => ( + + {i + 1} + + ))} +
+
+
+ + ) +} \ No newline at end of file diff --git a/src/styles/globals.css b/src/styles/globals.css index e88d14aa..d0080dda 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -5,115 +5,146 @@ @tailwind utilities; html, body { - @apply w-screen; - @apply text-base; - @apply text-gray-900; + @apply + w-screen + text-base + text-gray-900 + ; } @layer base { h1, .h1 { - @apply text-lg; - @apply font-semibold; - @apply text-gray-900; + @apply + text-lg + font-semibold + text-gray-900 + ; } a { - @apply font-medium; - @apply text-yellow-900; + @apply + font-medium + text-yellow-900 + ; } } @layer components { .badge-red { - @apply text-xs; - @apply font-medium; - @apply px-1; - @apply py-0.5; - @apply rounded; - @apply bg-red-300; - @apply text-red-900; + @apply + text-xs + font-medium + px-1 + py-0.5 + rounded + bg-red-300 + text-red-900 + ; } .badge-yellow { - @apply text-xs; - @apply font-medium; - @apply px-1; - @apply py-0.5; - @apply rounded; - @apply bg-yellow-300; - @apply text-yellow-900; + @apply + text-xs + font-medium + px-1 + py-0.5 + rounded + bg-yellow-300 + text-yellow-900 + ; } .form-label { - @apply text-sm; - @apply mb-1; - @apply block; + @apply + text-sm + mb-1 + block + ; } .form-input { - @apply p-3; - @apply rounded; - @apply border; - @apply border-gray-300; - @apply bg-transparent; - @apply w-full; - @apply leading-none; + @apply + p-3 + rounded + border + border-gray-300 + bg-transparent + w-full + leading-none + ; } .btn-primary { - @apply p-3; - @apply bg-yellow-900; - @apply border-yellow-900; - @apply rounded; - @apply border; - @apply text-white; - @apply text-center; - @apply disabled:bg-yellow-700; - @apply disabled:border-yellow-700; + @apply + block + w-fit + p-3 + bg-yellow-900 + border-yellow-900 + rounded + border + text-white + text-center + disabled:bg-yellow-700 + disabled:border-yellow-700 + ; } .btn-light { - @apply p-3; - @apply bg-gray-100; - @apply border-gray-300; - @apply rounded; - @apply border; - @apply text-gray-900; - @apply text-center; + @apply + block + w-fit + p-3 + bg-gray-100 + border-gray-300 + rounded + border + text-gray-900 + text-center + ; } .product-card { - @apply w-full; - @apply h-full; - @apply border; - @apply border-gray-300; - @apply rounded; - @apply relative; - @apply flex; - @apply flex-col; + @apply + w-full + h-full + border + border-gray-300 + rounded + relative + flex + flex-col + ; } .product-card__image { - @apply w-full; - @apply h-[160px]; - @apply object-contain; - @apply object-center; - @apply border-b; - @apply border-gray-300; + @apply + w-full + h-[160px] + object-contain + object-center + border-b + border-gray-300 + ; } .product-card__description { - @apply p-2; - @apply pb-3; - @apply flex; - @apply flex-col; - @apply flex-1; - @apply justify-between; + @apply + p-2 + pb-3 + flex + flex-col + flex-1 + justify-between + ; } .product-card__title { - @apply text-sm; - @apply text-gray-900; + @apply + text-sm + text-gray-900 + h-16 + ; } .product-card__brand { @@ -140,19 +171,21 @@ html, body { } .menu-wrapper { - @apply fixed; - @apply top-0; - @apply left-0; - @apply bg-white; - @apply w-[85%]; - @apply h-full; - @apply z-[60]; - @apply py-4; - @apply px-4; - @apply overflow-y-auto; - @apply translate-x-[-100%]; - @apply ease-linear; - @apply duration-150; + @apply + fixed + top-0 + left-0 + bg-white + w-[85%] + h-full + z-[60] + py-4 + px-4 + overflow-y-auto + translate-x-[-100%] + ease-linear + duration-150 + ; } .menu-wrapper.active{ @@ -160,51 +193,59 @@ html, body { } .menu-overlay { - @apply fixed; - @apply top-0; - @apply left-0; - @apply w-full; - @apply h-full; - @apply z-[55]; - @apply bg-gray-900/60; + @apply + fixed + top-0 + left-0 + w-full + h-full + z-[55] + bg-gray-900/60 + ; } .sticky-header { - @apply px-4; - @apply py-3; - @apply bg-white; - @apply border-b; - @apply sticky; - @apply top-0; - @apply shadow; - @apply z-50; + @apply + px-4 + py-3 + bg-white + border-b + sticky + top-0 + shadow + z-50 + ; } .content-container { - @apply max-w-full; - @apply overflow-x-hidden; + @apply + max-w-full + overflow-x-hidden + ; } #indoteknik_toast { - @apply fixed; - @apply bottom-4; - @apply translate-y-[200%]; - @apply left-[50%]; - @apply translate-x-[-50%]; - @apply z-[100]; - @apply flex; - @apply items-center; - @apply p-4; - @apply mb-4; - @apply w-[90%]; - @apply text-gray-500; - @apply bg-white; - @apply border; - @apply border-gray-300; - @apply rounded-lg; - @apply shadow; - @apply ease-linear; - @apply duration-300; + @apply + fixed + bottom-4 + translate-y-[200%] + left-[50%] + translate-x-[-50%] + z-[100] + flex + items-center + p-4 + mb-4 + w-[90%] + text-gray-500 + bg-white + border + border-gray-300 + rounded-lg + shadow + ease-linear + duration-300 + ; } #indoteknik_toast.active { -- cgit v1.2.3