From d6d2d9ceef2e95b604ac4ebdc054cad44a8440b1 Mon Sep 17 00:00:00 2001 From: IT Fixcomart Date: Mon, 31 Oct 2022 16:31:56 +0700 Subject: Product detail and header --- next.config.js | 17 +++ package.json | 5 +- pages/_app.js | 7 -- pages/api/hello.js | 5 - pages/index.js | 69 ------------ public/images/logo.png | Bin 0 -> 49879 bytes src/components/header.js | 68 ++++++++++++ src/components/productCard.js | 35 +++++++ src/helpers/apiOdoo.js | 21 ++++ src/helpers/currencyFormat.js | 8 ++ src/helpers/slug.js | 13 +++ src/icons/menu.svg | 5 + src/icons/search.svg | 4 + src/icons/shopping-cart.svg | 12 +++ src/pages/_app.js | 7 ++ src/pages/index.js | 12 +++ src/pages/shop/product/[slug].js | 172 ++++++++++++++++++++++++++++++ src/styles/globals.css | 220 +++++++++++++++++++++++++++++++++++++++ styles/Home.module.css | 129 ----------------------- styles/globals.css | 26 ----- tailwind.config.js | 83 ++++++++++++++- 21 files changed, 679 insertions(+), 239 deletions(-) delete mode 100644 pages/_app.js delete mode 100644 pages/api/hello.js delete mode 100644 pages/index.js create mode 100644 public/images/logo.png create mode 100644 src/components/header.js create mode 100644 src/components/productCard.js create mode 100644 src/helpers/apiOdoo.js create mode 100644 src/helpers/currencyFormat.js create mode 100644 src/helpers/slug.js create mode 100644 src/icons/menu.svg create mode 100644 src/icons/search.svg create mode 100644 src/icons/shopping-cart.svg create mode 100644 src/pages/_app.js create mode 100644 src/pages/index.js create mode 100644 src/pages/shop/product/[slug].js create mode 100644 src/styles/globals.css delete mode 100644 styles/Home.module.css delete mode 100644 styles/globals.css diff --git a/next.config.js b/next.config.js index ae887958..8a019dcc 100644 --- a/next.config.js +++ b/next.config.js @@ -2,6 +2,23 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, + webpack(config) { + config.module.rules.push({ + test: /\.svg$/i, + issuer: /\.[jt]sx?$/, + use: ['@svgr/webpack'], + }) + + return config + }, + env: { + DB_HOST: 'https://erp.indoteknik.com', + DB_NAME: 'erp_indoteknik', + // DB_HOST: 'http://192.168.27.253:8069', + // DB_NAME: 'indoteknik_odoo', + DB_USER: 'it@fixcomart.co.id', + DB_PASS: 'Fixcomart378', + }, } module.exports = nextConfig diff --git a/package.json b/package.json index e280d3e9..b64a34d7 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,14 @@ "lint": "next lint" }, "dependencies": { + "axios": "^1.1.3", "next": "13.0.0", "react": "18.2.0", - "react-dom": "18.2.0" + "react-dom": "18.2.0", + "swiper": "^8.4.4" }, "devDependencies": { + "@svgr/webpack": "^6.5.0", "autoprefixer": "^10.4.12", "eslint": "8.26.0", "eslint-config-next": "13.0.0", diff --git a/pages/_app.js b/pages/_app.js deleted file mode 100644 index 1e1cec92..00000000 --- a/pages/_app.js +++ /dev/null @@ -1,7 +0,0 @@ -import '../styles/globals.css' - -function MyApp({ Component, pageProps }) { - return -} - -export default MyApp diff --git a/pages/api/hello.js b/pages/api/hello.js deleted file mode 100644 index df63de88..00000000 --- a/pages/api/hello.js +++ /dev/null @@ -1,5 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction - -export default function handler(req, res) { - res.status(200).json({ name: 'John Doe' }) -} diff --git a/pages/index.js b/pages/index.js deleted file mode 100644 index dc4b6403..00000000 --- a/pages/index.js +++ /dev/null @@ -1,69 +0,0 @@ -import Head from 'next/head' -import Image from 'next/image' -import styles from '../styles/Home.module.css' - -export default function Home() { - return ( -
- - Create Next App - - - - -
-

- Welcome to Next.js! -

- -

- Get started by editing{' '} - pages/index.js -

- -
- -

Documentation →

-

Find in-depth information about Next.js features and API.

-
- - -

Learn →

-

Learn about Next.js in an interactive course with quizzes!

-
- - -

Examples →

-

Discover and deploy boilerplate example Next.js projects.

-
- - -

Deploy →

-

- Instantly deploy your Next.js site to a public URL with Vercel. -

-
-
-
- - -
- ) -} diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 00000000..87c696aa Binary files /dev/null and b/public/images/logo.png differ diff --git a/src/components/header.js b/src/components/header.js new file mode 100644 index 00000000..baebbd3a --- /dev/null +++ b/src/components/header.js @@ -0,0 +1,68 @@ +import Image from "next/image"; +import Link from "next/link"; +import ShoppingCartIcon from "../icons/shopping-cart.svg"; +import SearchIcon from "../icons/search.svg"; +import MenuIcon from "../icons/menu.svg"; +import { useState } from "react"; + + +export default function Header() { + const [isMenuActive, setIsMenuActive] = useState(false); + + const openMenu = () => setIsMenuActive(true); + const closeMenu = () => setIsMenuActive(false); + + return ( + <> +
+
+ Masuk + Daftar +
+
+ + Brand +
+ +
+ + + Blog +
+ +
+ + +
+
+ + +
+
+ + Logo Indoteknik + +
+ + + + +
+
+
+ + +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/productCard.js b/src/components/productCard.js new file mode 100644 index 00000000..dc292316 --- /dev/null +++ b/src/components/productCard.js @@ -0,0 +1,35 @@ +import Link from "next/link"; +import currencyFormat from "../helpers/currencyFormat"; +import { createSlug } from "../helpers/slug"; + +export default function productCard({ data }) { + let product = data; + return ( +
+ + {product.name} + +
+
+ {typeof product.manufacture.name !== undefined ? ( + {product.manufacture.name} + ) : ( + - + )} + + {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'}

+
+
+
+ ) +} \ No newline at end of file diff --git a/src/helpers/apiOdoo.js b/src/helpers/apiOdoo.js new file mode 100644 index 00000000..632edf61 --- /dev/null +++ b/src/helpers/apiOdoo.js @@ -0,0 +1,21 @@ +const axios = require('axios'); + +const getOdoo = async (url) => { + try { + let res = await axios(process.env.DB_HOST + url, { + headers: { + db: process.env.DB_NAME, + username: process.env.DB_USER, + password: process.env.DB_PASS, + } + }); + + return res.data.result || []; + } catch (error) { + console.log(error); + } +} + +export { + getOdoo, +}; \ No newline at end of file diff --git a/src/helpers/currencyFormat.js b/src/helpers/currencyFormat.js new file mode 100644 index 00000000..dadeaec6 --- /dev/null +++ b/src/helpers/currencyFormat.js @@ -0,0 +1,8 @@ +export default function currencyFormat(value) { + const currency = new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + maximumFractionDigits: 0 + }); + return currency.format(value); +} \ No newline at end of file diff --git a/src/helpers/slug.js b/src/helpers/slug.js new file mode 100644 index 00000000..5a8db08f --- /dev/null +++ b/src/helpers/slug.js @@ -0,0 +1,13 @@ +const createSlug = (name, id) => { + return name.trim().replace(new RegExp(/[^A-Za-z0-9]/, 'g'), '-').toLowerCase() + '-' + id; +} + +const getId = (slug) => { + let id = slug.split('-'); + return id[id.length-1]; +} + +export { + createSlug, + getId +}; \ No newline at end of file diff --git a/src/icons/menu.svg b/src/icons/menu.svg new file mode 100644 index 00000000..f4ff45ad --- /dev/null +++ b/src/icons/menu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/search.svg b/src/icons/search.svg new file mode 100644 index 00000000..6de1cdfa --- /dev/null +++ b/src/icons/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/shopping-cart.svg b/src/icons/shopping-cart.svg new file mode 100644 index 00000000..5c899876 --- /dev/null +++ b/src/icons/shopping-cart.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/pages/_app.js b/src/pages/_app.js new file mode 100644 index 00000000..1e1cec92 --- /dev/null +++ b/src/pages/_app.js @@ -0,0 +1,7 @@ +import '../styles/globals.css' + +function MyApp({ Component, pageProps }) { + return +} + +export default MyApp diff --git a/src/pages/index.js b/src/pages/index.js new file mode 100644 index 00000000..57f96ec9 --- /dev/null +++ b/src/pages/index.js @@ -0,0 +1,12 @@ +import { useEffect, useState } from "react"; +import Header from "../components/header"; + +export default function Home() { + const [product, setProduct] = useState({}); + + return ( + <> +
+ + ) +} diff --git a/src/pages/shop/product/[slug].js b/src/pages/shop/product/[slug].js new file mode 100644 index 00000000..519f8160 --- /dev/null +++ b/src/pages/shop/product/[slug].js @@ -0,0 +1,172 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import Header from "../../../components/header"; +import ProductCard from "../../../components/productCard"; +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 'swiper/css'; + + +export async function getServerSideProps(context) { + const { slug } = context.query; + let product = await getOdoo('/api/v1/product/' + getId(slug)); + product = product[0]; + + const similarProducts = await getOdoo(`/api/v1/product/${getId(slug)}/similar?limit=20`); + + return {props: {product, similarProducts}}; +} + +export default function ProductDetail({product, similarProducts}) { + 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: '', + }); + + useEffect(() => { + if (product.variants.length == 1) { + setSelectedVariant(product.variants[0].id); + } + }, [product.variants]); + + 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]) + + let onchangeVariant = (e) => { + setSelectedVariant(e.target.value); + setQuantity("1"); + } + + let addToCart = () => { + return true; + } + + return ( + <> + + {product.name + '- Indoteknik'} + +
+
+ {product.name} + + {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)} /> +
+
+ +
+ + +
+ +
+

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

+
+
+ +
+

Deskripsi Produk

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

Produk Lainnya

+ + {similarProducts.products.map((product, index) => ())} + +
+ +
+ + ); +} \ No newline at end of file diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100644 index 00000000..763a6d39 --- /dev/null +++ b/src/styles/globals.css @@ -0,0 +1,220 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, body { + @apply max-w-full; + @apply text-base; + @apply text-gray-900; +} + +@layer base { + h1, .h1 { + @apply text-lg; + @apply font-semibold; + @apply text-gray-900; + } + + a { + @apply font-medium; + @apply 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; + } + + .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; + } + + .form-label { + @apply text-sm; + @apply mb-1; + @apply block; + } + + .form-input { + @apply p-3; + @apply rounded; + @apply border; + @apply border-gray-300; + @apply bg-transparent; + @apply w-full; + @apply 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; + } + + .btn-light { + @apply p-3; + @apply bg-gray-100; + @apply border-gray-300; + @apply rounded; + @apply border; + @apply text-gray-900; + @apply 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; + } + + .product-card__image { + @apply w-full; + @apply h-[160px]; + @apply object-contain; + @apply object-center; + @apply border-b; + @apply border-gray-300; + } + + .product-card__description { + @apply p-2; + @apply pb-3; + @apply flex; + @apply flex-col; + @apply flex-1; + @apply justify-between; + } + + .product-card__title { + @apply text-sm; + @apply text-gray-900; + } + + .product-card__brand { + @apply text-sm; + } +} + +@layer utilities { + .wrap-line-ellipsis-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + } + + .wrap-line-ellipsis-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.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; +} + +.menu-wrapper.active{ + @apply translate-x-0; +} + +.menu-overlay { + @apply fixed; + @apply top-0; + @apply left-0; + @apply w-full; + @apply h-full; + @apply z-[55]; + @apply 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; +} + +.content-container { + @apply max-w-full; + @apply 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; +} + +#indoteknik_toast.active { + @apply translate-y-0; +} + +.category-menu { + @apply hidden; +} + +.swiper-slide { + @apply !h-auto; +} \ No newline at end of file diff --git a/styles/Home.module.css b/styles/Home.module.css deleted file mode 100644 index bd50f42f..00000000 --- a/styles/Home.module.css +++ /dev/null @@ -1,129 +0,0 @@ -.container { - padding: 0 2rem; -} - -.main { - min-height: 100vh; - padding: 4rem 0; - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.footer { - display: flex; - flex: 1; - padding: 2rem 0; - border-top: 1px solid #eaeaea; - justify-content: center; - align-items: center; -} - -.footer a { - display: flex; - justify-content: center; - align-items: center; - flex-grow: 1; -} - -.title a { - color: #0070f3; - text-decoration: none; -} - -.title a:hover, -.title a:focus, -.title a:active { - text-decoration: underline; -} - -.title { - margin: 0; - line-height: 1.15; - font-size: 4rem; -} - -.title, -.description { - text-align: center; -} - -.description { - margin: 4rem 0; - line-height: 1.5; - font-size: 1.5rem; -} - -.code { - background: #fafafa; - border-radius: 5px; - padding: 0.75rem; - font-size: 1.1rem; - font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, - Bitstream Vera Sans Mono, Courier New, monospace; -} - -.grid { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - max-width: 800px; -} - -.card { - margin: 1rem; - padding: 1.5rem; - text-align: left; - color: inherit; - text-decoration: none; - border: 1px solid #eaeaea; - border-radius: 10px; - transition: color 0.15s ease, border-color 0.15s ease; - max-width: 300px; -} - -.card:hover, -.card:focus, -.card:active { - color: #0070f3; - border-color: #0070f3; -} - -.card h2 { - margin: 0 0 1rem 0; - font-size: 1.5rem; -} - -.card p { - margin: 0; - font-size: 1.25rem; - line-height: 1.5; -} - -.logo { - height: 1em; - margin-left: 0.5rem; -} - -@media (max-width: 600px) { - .grid { - width: 100%; - flex-direction: column; - } -} - -@media (prefers-color-scheme: dark) { - .card, - .footer { - border-color: #222; - } - .code { - background: #111; - } - .logo img { - filter: invert(1); - } -} diff --git a/styles/globals.css b/styles/globals.css deleted file mode 100644 index 4f184216..00000000 --- a/styles/globals.css +++ /dev/null @@ -1,26 +0,0 @@ -html, -body { - padding: 0; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -} - -a { - color: inherit; - text-decoration: none; -} - -* { - box-sizing: border-box; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } - body { - color: white; - background: black; - } -} diff --git a/tailwind.config.js b/tailwind.config.js index 32e3abde..127739a6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,8 +1,87 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: [], + content: [ + "./src/pages/**/*.{js,ts,tsx}", + "./src/components/**/*.{js,ts,tsx}", + ], theme: { - extend: {}, + extend: { + fontFamily: { + 'sans': ['Inter', 'sans-serif'] + }, + fontSize: { + base: '15px', + lg: '18px', + }, + colors: { + gray: { + 900: '#2B2B2B', + 800: '#595959', + 700: '#767676', + 600: '#888888', + 500: '#ABABAB', + 400: '#CCCCCC', + 300: '#E4E4E4', + 200: '#F0F0F0', + 100: '#F6F6F6', + }, + red: { + 900: '#CE1D34', + 800: '#D94F61', + 700: '#DF6D7C', + 600: '#E3808D', + 500: '#ECA6AF', + 400: '#F3C9CE', + 300: '#F9E3E6', + 200: '#FBEFF0', + 100: '#FDF6F7', + }, + yellow: { + 900: '#D7A30A', + 800: '#DFB740', + 700: '#E5C361', + 600: '#E8CB75', + 500: '#EFDB9E', + 400: '#F5E8C2', + 300: '#FAF3E0', + 200: '#FCF8ED', + 100: '#FDFBF5', + }, + // yellow500: { + // 900: '#F8C20A', + // 800: '#FACF40', + // 700: '#FBD761', + // 600: '#FBDC75', + // 500: '#FCE79E', + // 400: '#FDF0C2', + // 300: '#FEF7E0', + // 200: '#FFFBED', + // 100: '#FFFCF5', + // } + // yellow600: { + // 900: '#D7A30A', + // 800: '#DFB740', + // 700: '#E5C361', + // 600: '#E8CB75', + // 500: '#EFDB9E', + // 400: '#F5E8C2', + // 300: '#FAF3E0', + // 200: '#FCF8ED', + // 100: '#FDFBF5', + // } + // yellow700: { + // 900: '#B5860A', + // 800: '#C5A040', + // 700: '#CFB161', + // 600: '#D5BB75', + // 500: '#E2CF9E', + // 400: '#EDE1C2', + // 300: '#F6F0E0', + // 200: '#FAF6ED', + // 100: '#FCFAF5', + // } + } + }, }, plugins: [], } -- cgit v1.2.3