From 690bb837f68aadf0d02438b6d02e08968dbe67dd Mon Sep 17 00:00:00 2001 From: Vassshhh Date: Fri, 1 Aug 2025 21:14:12 +0700 Subject: [PATCH] ok --- public/cart-cross-svgrepo-com.svg | 7 + public/cart-plus-svgrepo-com.svg | 7 + public/cart-shopping-svgrepo-com.svg | 4 + src/App.js | 9 +- src/components/ProductDetail.module.css | 47 ++++- src/components/ProductDetailPage.js | 234 +++++++++++++++++------- src/components/ProductSection.js | 55 ++++-- src/components/pages/ProductsPage.js | 189 ++++++++++++++----- 8 files changed, 423 insertions(+), 129 deletions(-) create mode 100644 public/cart-cross-svgrepo-com.svg create mode 100644 public/cart-plus-svgrepo-com.svg create mode 100644 public/cart-shopping-svgrepo-com.svg diff --git a/public/cart-cross-svgrepo-com.svg b/public/cart-cross-svgrepo-com.svg new file mode 100644 index 0000000..09b79d1 --- /dev/null +++ b/public/cart-cross-svgrepo-com.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/cart-plus-svgrepo-com.svg b/public/cart-plus-svgrepo-com.svg new file mode 100644 index 0000000..9fd8f7d --- /dev/null +++ b/public/cart-plus-svgrepo-com.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/cart-shopping-svgrepo-com.svg b/public/cart-shopping-svgrepo-com.svg new file mode 100644 index 0000000..c722911 --- /dev/null +++ b/public/cart-shopping-svgrepo-com.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/App.js b/src/App.js index 0c1cf07..216245c 100644 --- a/src/App.js +++ b/src/App.js @@ -105,16 +105,17 @@ function App() { if (data && data[0] && data[0].token) { // Update token with data[0].token document.cookie = `token=${data[0].token}; path=/`; + + const payload = parseJwt(token); + if (payload && payload.username) { + setUsername(payload.username); + } } else { console.warn('Token tidak ditemukan dalam data.'); } }) .catch(err => console.error('Fetch error:', err)); - const payload = parseJwt(token); - if (payload && payload.username) { - setUsername(payload.username); - } } }, []); const scrollToProduct = () => { diff --git a/src/components/ProductDetail.module.css b/src/components/ProductDetail.module.css index c48ab22..947a59a 100644 --- a/src/components/ProductDetail.module.css +++ b/src/components/ProductDetail.module.css @@ -10,7 +10,7 @@ } .image { - width: 100%; + width: 40vw; height: 260px; background-color: #e2e8f0; border-radius: 0.75rem; @@ -20,6 +20,10 @@ color: #64748b; font-size: 1rem; margin-bottom: 1.5rem; + + background-repeat: no-repeat; + background-size: cover; + background-position: center; } .headerRow { @@ -32,14 +36,14 @@ } .title { - font-size: 1.1rem; + font-size: 0.9rem; font-weight: bold; color: #1e293b; margin: 0; } .price { - font-size: 1.1rem; + font-size: 0.9rem; font-weight: bold; color: #2563eb; /* default color, bisa override di inline style */ } @@ -92,4 +96,41 @@ .buttonGroup { gap: 0.5rem; } + +.image { + width: 63vw; +} +} + +.childSelector { + background: white; + color: black; + text-align: left; +} + +.childProduct { + display: block; + margin-bottom: 8px; + border: 1px solid black; + padding: 10px; + border-radius: 15px; +} + +.confirmButton { + background-color: #2563eb; + color: white; + padding: 8px 16px; + margin-right: 10px; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.cancelButton { + background-color: #f87171; + color: white; + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; } diff --git a/src/components/ProductDetailPage.js b/src/components/ProductDetailPage.js index f43e00f..9520b30 100644 --- a/src/components/ProductDetailPage.js +++ b/src/components/ProductDetailPage.js @@ -3,6 +3,8 @@ import styles from './ProductDetail.module.css'; const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => { const [inCart, setInCart] = useState(false); + const [showChildSelector, setShowChildSelector] = useState(false); + const [selectedChildIds, setSelectedChildIds] = useState([]); useEffect(() => { const existingCookie = document.cookie @@ -47,85 +49,193 @@ const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => { setInCart(true); } - document.cookie = `itemsId=${JSON.stringify(updatedItems)}; path=/; max-age=${7 * 24 * 60 * 60 - }`; + document.cookie = `itemsId=${JSON.stringify(updatedItems)}; path=/; max-age=${7 * 24 * 60 * 60}`; }; - const onCheckout = () => { - // Ambil token dari cookie - const tokenCookie = document.cookie - .split('; ') - .find(row => row.startsWith('token=')); - const token = tokenCookie ? tokenCookie.split('=')[1] : ''; +const onCheckout = () => { + const tokenCookie = document.cookie + .split('; ') + .find(row => row.startsWith('token=')); + const token = tokenCookie ? tokenCookie.split('=')[1] : ''; - // Ambil itemsId dari cookie - const itemsCookie = document.cookie - .split('; ') - .find(row => row.startsWith('itemsId=')); + if (!tokenCookie) { + setPostLoginAction(() => () => onCheckout()); + setShowedModal('login'); + return; + } - let items = []; - if (itemsCookie) { - try { - items = JSON.parse(itemsCookie.split('=')[1]); - if (!Array.isArray(items)) items = []; - } catch (e) { - items = []; - } + // Jika punya children, tampilkan pilihan + if (product.children && product.children.length > 0) { + setShowChildSelector(true); + return; + } + + // Ambil itemsId dari cookie + const itemsCookie = document.cookie + .split('; ') + .find(row => row.startsWith('itemsId=')); + + let items = []; + if (itemsCookie) { + try { + items = JSON.parse(itemsCookie.split('=')[1]); + if (!Array.isArray(items)) items = []; + } catch (e) { + items = []; } + } - if (!items.includes(product.id)) { - items.push(product.id); + // Tambahkan product.id jika belum ada + if (!items.includes(product.id)) { + items.push(product.id); + } + + const itemsParam = JSON.stringify(items); + + window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`; +}; + +const onConfirmChildren = () => { + const tokenCookie = document.cookie + .split('; ') + .find(row => row.startsWith('token=')); + const token = tokenCookie ? tokenCookie.split('=')[1] : ''; + + if (selectedChildIds.length === 0) { + alert('Pilih minimal satu produk'); + return; + } + + // Ambil itemsId dari cookie + const itemsCookie = document.cookie + .split('; ') + .find(row => row.startsWith('itemsId=')); + + let items = []; + if (itemsCookie) { + try { + items = JSON.parse(itemsCookie.split('=')[1]); + if (!Array.isArray(items)) items = []; + } catch (e) { + items = []; } - // Encode items ke string untuk query param - const itemsParam = JSON.stringify(items); + } - if (!tokenCookie) { - setPostLoginAction(() => () => onCheckout()); // remember intent - setShowedModal('login'); - return; - } + // Gabungkan items dari cookie dengan selectedChildIds + const mergedItems = Array.from(new Set([...items, ...selectedChildIds])); - // Redirect dengan token dan itemsId di query route ke checkout.kediritechnopark.com - window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`; - }; + const itemsParam = JSON.stringify(mergedItems); + + window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`; +}; - // Override harga warna jika free const priceColor = product.price === 0 ? '#059669' : '#2563eb'; return (
-
📦
-
-

{product.name}

-
- {product.price == null - ? 'Pay-As-You-Go' - : `Rp ${parseInt(product.price).toLocaleString('id-ID')}`} + {/* ✅ Tampilan utama disembunyikan jika sedang memilih child */} + {!showChildSelector && ( + <> +
+ +
+

{product.name}

+
+ {product.price == null + ? 'Pay-As-You-Go' + : `Rp ${parseInt(product.price).toLocaleString('id-ID')}`} +
+
+ +

{product.description}

+ +
+ + + +
+ + )} + + {/* ✅ UI pemilihan child */} + {showChildSelector && ( +
+

Pilih Paket

+ {product.children.map(child => ( + + ))} + +

+ Total Harga:{' '} + Rp {selectedChildIds + .map(id => { + const found = product.children.find(child => child.id === id); + return found ? found.price || 0 : 0; + }) + .reduce((a, b) => a + b, 0) + .toLocaleString('id-ID')} +

+ +
+ + + +
-
- -

{product.description}

- -
- - -
+ )}
); }; diff --git a/src/components/ProductSection.js b/src/components/ProductSection.js index 1d862d7..85135ae 100644 --- a/src/components/ProductSection.js +++ b/src/components/ProductSection.js @@ -6,18 +6,48 @@ import styles from './Styles.module.css'; const ProductSection = ({ hoveredCard, setHoveredCard, setSelectedProduct, setShowedModal, productSectionRef }) => { const [products, setProducts] = useState([]); - useEffect(() => { - fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ type: 'product', onlyParents: true }), +useEffect(() => { + fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ type: 'product' }), + }) + .then(res => res.json()) + .then(data => { + const parentMap = {}; + const childrenMap = {}; + + // Pisahkan parent dan child + data.forEach(product => { + if (product.sub_product_of) { + const parentId = product.sub_product_of; + if (!childrenMap[parentId]) childrenMap[parentId] = []; + childrenMap[parentId].push(product); + } else { + parentMap[product.id] = { + ...product, + children: [] + }; + } + }); + + // Pasang children ke parent + Object.keys(childrenMap).forEach(parentId => { + const parent = parentMap[parentId]; + if (parent) { + parent.children = childrenMap[parentId]; + } + }); + + // Ambil parent saja + const enrichedData = Object.values(parentMap); + + setProducts(enrichedData); }) - .then(res => res.json()) - .then(data => setProducts(data)) - .catch(err => console.error('Fetch error:', err)); - }, []); + .catch(err => console.error('Fetch error:', err)); +}, []); return ( @@ -31,7 +61,8 @@ const ProductSection = ({ hoveredCard, setHoveredCard, setSelectedProduct, setSh
{products && products[0]?.name && - products.map(product => ( + products + .map(product => (
p.product_id); + return [...new Set(productIds)]; +} + +function getLatestEndDatesFromJwt(token) { + const payload = parseJwt(token); + if (!payload || !payload.subscriptions || !payload.subscriptions) return {}; + + const result = {}; + payload.subscriptions.forEach(p => { + if (!p.end_date) return; + const id = p.product_id; + const endDate = new Date(p.end_date); + if (!result[id] || endDate > new Date(result[id])) { + result[id] = p.end_date; + } + }); + + return result; +} +function getTotalTokenFromJwt(token) { + const payload = parseJwt(token); + if (!payload || !payload.subscriptions || !payload.subscriptions) return {}; + + const tokenQuantities = {}; + payload.subscriptions.forEach(p => { + // Pastikan ada quantity dan unit_type token + if (p.quantity && p.product_id) { + tokenQuantities[p.product_id] = (tokenQuantities[p.product_id] || 0) + p.quantity; + } + }); + + return tokenQuantities; +} + + const CoursePage = () => { const [postLoginAction, setPostLoginAction] = useState(null); const [selectedProduct, setSelectedProduct] = useState({}); const [hoveredCard, setHoveredCard] = useState(null); - const [showedModal, setShowedModal] = useState(null); // 'product' | 'login' | null + const [showedModal, setShowedModal] = useState(null); const [products, setProducts] = useState([]); +useEffect(() => { + const match = document.cookie.match(new RegExp('(^| )token=([^;]+)')); + if (match) { + const token = match[2]; - useEffect(() => { - // Ambil token dari cookies - const match = document.cookie.match(new RegExp('(^| )token=([^;]+)')); - if (match) { - const token = match[2]; + const productIds = getDistinctProductIdsFromJwt(token); + const endDates = getLatestEndDatesFromJwt(token); + const tokenQuantitiesFromJwt = getTotalTokenFromJwt(token); - fetch('https://bot.kediritechnopark.com/webhook/users-dev/my-products', { + fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ type: 'product' }), + body: JSON.stringify({ itemsId: productIds, type: 'product' }), }) .then(res => res.json()) - .then(data => setProducts(data)) - .catch(err => console.error('Fetch error:', err)); + .then(data => { + const parentMap = {}; + const childrenMap = {}; + + data.forEach(product => { + if (product.sub_product_of) { + const parentId = product.sub_product_of; + if (!childrenMap[parentId]) childrenMap[parentId] = []; + childrenMap[parentId].push(product); + } else { + parentMap[product.id] = { + ...product, + quantity: product.quantity || 0, + end_date: endDates[product.id] || null, + children: [] + }; + } + }); +// ... + +Object.keys(childrenMap).forEach(parentId => { + const parent = parentMap[parentId]; + const children = childrenMap[parentId]; + + if (parent) { + parent.children = children; + + // Pakai quantity dari JWT langsung (tokenQuantitiesFromJwt) + parent.quantity = children.reduce((total, child) => { + return total + (tokenQuantitiesFromJwt[child.id] || 0); + }, 0); + } +}); + +// ... + +// Update quantity untuk produk yang bukan parent dan bukan anak +Object.values(parentMap).forEach(product => { + if (!product.children.length) { + if (product.unit_type === 'token') { + product.quantity = tokenQuantitiesFromJwt[product.id] || 0; } - }, []); + } +}); + + const enrichedData = Object.values(parentMap); + setProducts(enrichedData); + console.log(enrichedData); + }) + .catch(err => console.error('Fetch error:', err)); + } +}, []); + const features = [ { @@ -70,49 +160,52 @@ const CoursePage = () => { return (
- + {/* Courses Section */}
-

OUR COURSES

+

MY PRODUCTS

{products && products[0]?.name && - products.map(product => ( -
{ - setSelectedProduct(product); - setShowedModal('product'); - }} - onMouseEnter={() => setHoveredCard(product.id)} - onMouseLeave={() => setHoveredCard(null)} - > -
- {product.price == 0 && ( - Free - )} -
-
-

{product.name}

-

{product.description}

-
- - {product.price == 0 - ? 'Free' - : `Rp ${product.price.toLocaleString('id-ID')}`} - + products + .map(product => ( +
{ + setSelectedProduct(product); + setShowedModal('product'); + }} + onMouseEnter={() => setHoveredCard(product.id)} + onMouseLeave={() => setHoveredCard(null)} + > +
+ {product.price == 0 && ( + Free + )} +
+
+

{product.name}

+

{product.description}

+
+ + {product.unit_type === 'duration' + ? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : 'N/A'}` + : `SISA TOKEN ${product.quantity || 0}` + } + + +
-
- ))} + ))}