summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIT Fixcomart <it@fixcomart.co.id>2022-10-31 16:31:56 +0700
committerIT Fixcomart <it@fixcomart.co.id>2022-10-31 16:31:56 +0700
commitd6d2d9ceef2e95b604ac4ebdc054cad44a8440b1 (patch)
tree376278eb3f7baa6b9405177b8fb57805d4a066b9
parent97c98e0a6fa0757e58ca9dacf8e720a52e10dcf5 (diff)
Product detail and header
-rw-r--r--next.config.js17
-rw-r--r--package.json5
-rw-r--r--pages/api/hello.js5
-rw-r--r--pages/index.js69
-rw-r--r--public/images/logo.pngbin0 -> 49879 bytes
-rw-r--r--src/components/header.js68
-rw-r--r--src/components/productCard.js35
-rw-r--r--src/helpers/apiOdoo.js21
-rw-r--r--src/helpers/currencyFormat.js8
-rw-r--r--src/helpers/slug.js13
-rw-r--r--src/icons/menu.svg5
-rw-r--r--src/icons/search.svg4
-rw-r--r--src/icons/shopping-cart.svg12
-rw-r--r--src/pages/_app.js (renamed from pages/_app.js)0
-rw-r--r--src/pages/index.js12
-rw-r--r--src/pages/shop/product/[slug].js172
-rw-r--r--src/styles/globals.css220
-rw-r--r--styles/Home.module.css129
-rw-r--r--styles/globals.css26
-rw-r--r--tailwind.config.js83
20 files changed, 672 insertions, 232 deletions
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/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 (
- <div className={styles.container}>
- <Head>
- <title>Create Next App</title>
- <meta name="description" content="Generated by create next app" />
- <link rel="icon" href="/favicon.ico" />
- </Head>
-
- <main className={styles.main}>
- <h1 className={styles.title}>
- Welcome to <a href="https://nextjs.org">Next.js!</a>
- </h1>
-
- <p className={styles.description}>
- Get started by editing{' '}
- <code className={styles.code}>pages/index.js</code>
- </p>
-
- <div className={styles.grid}>
- <a href="https://nextjs.org/docs" className={styles.card}>
- <h2>Documentation &rarr;</h2>
- <p>Find in-depth information about Next.js features and API.</p>
- </a>
-
- <a href="https://nextjs.org/learn" className={styles.card}>
- <h2>Learn &rarr;</h2>
- <p>Learn about Next.js in an interactive course with quizzes!</p>
- </a>
-
- <a
- href="https://github.com/vercel/next.js/tree/canary/examples"
- className={styles.card}
- >
- <h2>Examples &rarr;</h2>
- <p>Discover and deploy boilerplate example Next.js projects.</p>
- </a>
-
- <a
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
- className={styles.card}
- >
- <h2>Deploy &rarr;</h2>
- <p>
- Instantly deploy your Next.js site to a public URL with Vercel.
- </p>
- </a>
- </div>
- </main>
-
- <footer className={styles.footer}>
- <a
- href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- Powered by{' '}
- <span className={styles.logo}>
- <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
- </span>
- </a>
- </footer>
- </div>
- )
-}
diff --git a/public/images/logo.png b/public/images/logo.png
new file mode 100644
index 00000000..87c696aa
--- /dev/null
+++ b/public/images/logo.png
Binary files 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 (
+ <>
+ <div className={isMenuActive ? 'menu-wrapper active' : 'menu-wrapper'}>
+ <div className="flex gap-x-2 items-center">
+ <Link href="/login" className="w-full py-2 btn-light">Masuk</Link>
+ <Link href="/register" className="w-full py-2 btn-primary">Daftar</Link>
+ </div>
+ <div className="flex flex-col gap-y-4 mt-5">
+ <Link className="flex w-full font-normal text-gray-800" href="/shop/brands">
+ <span>Brand</span>
+ <div className="ml-auto">
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="stroke-gray-700 feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
+ </div>
+ </Link>
+ <Link className="flex w-full font-normal text-gray-800" href="/blog">
+ <span>Blog</span>
+ <div className="ml-auto">
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="stroke-gray-700 feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
+ </div>
+ </Link>
+ <button className="flex w-full font-normal text-gray-800" id="open_category_parent_menu">
+ <span>Kategori</span>
+ <div className="ml-auto">
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="stroke-gray-700 feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
+ </div>
+ </button>
+ </div>
+ </div>
+ <div className={isMenuActive ? 'menu-overlay block opacity-100' : 'menu-overlay hidden opacity-0'} onClick={closeMenu}></div>
+
+ <div className="sticky-header">
+ <div className="flex justify-between items-center">
+ <Link href="/">
+ <Image src="/images/logo.png" alt="Logo Indoteknik" width={120} height={40} />
+ </Link>
+ <div className="flex gap-4">
+ <Link href="/shop/cart">
+ <ShoppingCartIcon className="w-6" />
+ </Link>
+ <button onClick={openMenu}>
+ <MenuIcon className="w-6" />
+ </button>
+ </div>
+ </div>
+ <form action="" method="GET" className="flex mt-2">
+ <input type="text" name="product_name" className="form-input rounded-r-none border-r-0 focus:outline-none" placeholder="Ketikan nama, merek, part number"/>
+ <button type="submit" aria-label="search" className="btn-light bg-transparent px-2 py-1 rounded-l-none border-l-0">
+ <SearchIcon />
+ </button>
+ </form>
+ </div>
+ </>
+ )
+} \ 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 (
+ <div className="product-card">
+ <Link href={'/shop/product/' + createSlug(product.name, product.id)} className="block">
+ <img src={product.image} alt={product.name} className="product-card__image" loading="lazy" />
+ </Link>
+ <div className="product-card__description">
+ <div>
+ {typeof product.manufacture.name !== undefined ? (
+ <a href={'/shop/brands/' + createSlug(product.manufacture.name, product.manufacture.id)} className="product-card__brand">{product.manufacture.name}</a>
+ ) : (
+ <span className="product-card__brand">-</span>
+ )}
+ <a href={'/shop/product/' + createSlug(product.name, product.id)} className="product-card__title wrap-line-ellipsis-3">
+ {product.name}
+ </a>
+ </div>
+ <div>
+ {product.lowest_price.discount_percentage > 0 ? (
+ <div className="flex gap-x-1 items-center mt-2">
+ <span className="badge-yellow">{product.lowest_price.discount_percentage}%</span>
+ <p className="text-xs text-gray-800 line-through">{currencyFormat(product.lowest_price.price)}</p>
+ </div>
+ ) : ''}
+ <p className="text-sm text-gray-900 font-semibold">{product.lowest_price.price_discount > 0 ? currencyFormat(product.lowest_price.price_discount) : 'Tanya harga'}</p>
+ </div>
+ </div>
+ </div>
+ )
+} \ 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 @@
+<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.04395 10H18.0439" stroke="#2B2B2B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.04395 5H18.0439" stroke="#2B2B2B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.04395 15H18.0439" stroke="#2B2B2B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
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 @@
+<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.71061 15.8333C13.3925 15.8333 16.3773 12.8486 16.3773 9.16667C16.3773 5.48477 13.3925 2.5 9.71061 2.5C6.02871 2.5 3.04395 5.48477 3.04395 9.16667C3.04395 12.8486 6.02871 15.8333 9.71061 15.8333Z" stroke="#2B2B2B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M18.0439 17.5L14.4189 13.875" stroke="#2B2B2B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
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 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_10_13)">
+<path d="M7.50008 18.3334C7.96032 18.3334 8.33341 17.9603 8.33341 17.5C8.33341 17.0398 7.96032 16.6667 7.50008 16.6667C7.03984 16.6667 6.66675 17.0398 6.66675 17.5C6.66675 17.9603 7.03984 18.3334 7.50008 18.3334Z" stroke="#2B2B2B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M16.6666 18.3334C17.1268 18.3334 17.4999 17.9603 17.4999 17.5C17.4999 17.0398 17.1268 16.6667 16.6666 16.6667C16.2063 16.6667 15.8333 17.0398 15.8333 17.5C15.8333 17.9603 16.2063 18.3334 16.6666 18.3334Z" stroke="#2B2B2B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M0.833252 0.833313H4.16659L6.39992 11.9916C6.47612 12.3753 6.68484 12.7199 6.98954 12.9652C7.29424 13.2105 7.6755 13.3408 8.06659 13.3333H16.1666C16.5577 13.3408 16.9389 13.2105 17.2436 12.9652C17.5483 12.7199 17.757 12.3753 17.8333 11.9916L19.1666 4.99998H4.99992" stroke="#2B2B2B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_10_13">
+<rect width="20" height="20" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/pages/_app.js b/src/pages/_app.js
index 1e1cec92..1e1cec92 100644
--- a/pages/_app.js
+++ b/src/pages/_app.js
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 (
+ <>
+ <Header />
+ </>
+ )
+}
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 (
+ <>
+ <Head>
+ <title>{product.name + '- Indoteknik'}</title>
+ </Head>
+ <Header />
+ <div className="px-4 pt-5 pb-10">
+ <img src={product.image} alt={product.name} className="border border-gray-300 rounded-md mb-4 w-full h-[300px] object-contain object-center" />
+ <Link href={'/shop/brands/' + createSlug(product.manufacture.name, product.manufacture.id)}>
+ {product.manufacture.name || '-'}
+ </Link>
+ <h1 className="mb-3">{product.name}{activeVariant.attributes != '' ? ' - ' + activeVariant.attributes : ''}</h1>
+
+ {product.variant_total > 1 && selectedVariant == "" ? (
+ <p className="text-xs text-gray-800 mb-1">Harga mulai dari:</p>
+ ) : ''}
+
+ {product.lowest_price.discount_percentage > 0 ? (
+ <div className="flex gap-x-1 items-center">
+ <span className="badge-yellow">{activeVariant.price.discount_percentage}%</span>
+ <p className="text-xs text-gray-800 line-through">{currencyFormat(activeVariant.price.price)}</p>
+ </div>
+ ) : ''}
+
+ {product.lowest_price.price > 0 ? (
+ <p className="text-lg text-gray-900 font-semibold">{currencyFormat(activeVariant.price.price_discount)}</p>
+ ) : (
+ <p className="text-gray-800">Dapatkan harga terbaik, <a href="">hubungi kami.</a></p>
+ )}
+
+ <div className="flex gap-x-2 mt-5">
+ <div className="w-9/12">
+ <label className="form-label">Pilih: <span className="text-gray-800">{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>
+ <div className="w-3/12">
+ <label htmlFor="quantity" className="form-label">Jumlah</label>
+ <input type="number" name="quantity" id="quantity" className="form-input text-center is-invalid" value={quantity} onChange={(e) => setQuantity(e.target.value)} />
+ </div>
+ </div>
+
+ <div className="flex gap-x-2 mt-2">
+ <button className="btn-light w-full" >+ Quotation</button>
+ <button className="btn-primary w-full" onClick={addToCart} disabled={(product.lowest_price.price == 0 ? true : false)}>+ Keranjang</button>
+ </div>
+
+ <div className="mt-10">
+ <h2 className="h1 mb-2">Detail Produk</h2>
+ <div className="flex py-2 justify-between items-center gap-x-1 border-b border-gray-300">
+ <h3 className="text-gray-900">Jumlah Varian</h3>
+ <p className="text-gray-800">{product.variant_total} Varian</p>
+ </div>
+ <div className="flex py-2 justify-between items-center gap-x-1 border-b border-gray-300">
+ <h3 className="text-gray-900">Nomor SKU</h3>
+ <p className="text-gray-800" id="sku_number">SKU-{activeVariant.id}</p>
+ </div>
+ <div className="flex py-2 justify-between items-center gap-x-1 border-b border-gray-300">
+ <h3 className="text-gray-900">Part Number</h3>
+ <p className="text-gray-800" id="part_number">{activeVariant.code}</p>
+ </div>
+ <div className="flex py-2 justify-between items-center gap-x-1 border-b border-gray-300">
+ <h3 className="text-gray-900">Stok</h3>
+ <p className="text-gray-800" id="stock">
+ {activeVariant.stock > 0 ? (activeVariant.stock > 5 ? 'Lebih dari 5' : 'Kurang dari 5') : '0'}
+ </p>
+ </div>
+ <div className="flex py-2 justify-between items-center gap-x-1 border-b border-gray-300">
+ <h3 className="text-gray-900">Berat Barang</h3>
+ <p className="text-gray-800" id="weight">{activeVariant.weight > 0 ? activeVariant.weight : '1'} KG</p>
+ </div>
+ </div>
+
+ <div className="mt-10">
+ <h2 className="h1 mb-4">Deskripsi Produk</h2>
+ <div className="text-gray-800 leading-7" dangerouslySetInnerHTML={{__html: (product.description.trim() != '' ? product.description.replaceAll(/<*b>/g, '') : 'Belum ada deskripsi produk.')}}></div>
+ </div>
+
+ <div className="mt-10">
+ <h2 className="h1 mb-4">Produk Lainnya</h2>
+ <Swiper freeMode={true} slidesPerView={2.15} spaceBetween={8} loop={true}>
+ {similarProducts.products.map((product, index) => (<SwiperSlide key={index}><ProductCard data={product} /></SwiperSlide>))}
+ </Swiper>
+ </div>
+
+ </div>
+ </>
+ );
+} \ 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: [],
}