ok
This commit is contained in:
7
public/cart-cross-svgrepo-com.svg
Normal file
7
public/cart-cross-svgrepo-com.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.5" d="M2.08368 2.7512C2.22106 2.36044 2.64921 2.15503 3.03998 2.29242L3.34138 2.39838C3.95791 2.61511 4.48154 2.79919 4.89363 3.00139C5.33426 3.21759 5.71211 3.48393 5.99629 3.89979C6.27827 4.31243 6.39468 4.76515 6.44841 5.26153C6.47247 5.48373 6.48515 5.72967 6.49184 6H17.1301C18.815 6 20.3318 6 20.7757 6.57708C21.2197 7.15417 21.0461 8.02369 20.699 9.76275L20.1992 12.1875C19.8841 13.7164 19.7266 14.4808 19.1748 14.9304C18.6231 15.38 17.8426 15.38 16.2816 15.38H10.9787C8.18979 15.38 6.79534 15.38 5.92894 14.4662C5.06254 13.5523 4.9993 12.5816 4.9993 9.64L4.9993 7.03832C4.9993 6.29837 4.99828 5.80316 4.95712 5.42295C4.91779 5.0596 4.84809 4.87818 4.75783 4.74609C4.66977 4.61723 4.5361 4.4968 4.23288 4.34802C3.91003 4.18961 3.47128 4.03406 2.80367 3.79934L2.54246 3.7075C2.1517 3.57012 1.94629 3.14197 2.08368 2.7512Z" fill="#1C274C"/>
|
||||||
|
<path d="M12.0303 8.96967C11.7374 8.67678 11.2626 8.67678 10.9697 8.96967C10.6768 9.26256 10.6768 9.73744 10.9697 10.0303L11.9393 11L10.9697 11.9697C10.6768 12.2626 10.6768 12.7374 10.9697 13.0303C11.2626 13.3232 11.7374 13.3232 12.0303 13.0303L13 12.0607L13.9697 13.0303C14.2626 13.3232 14.7374 13.3232 15.0303 13.0303C15.3232 12.7374 15.3232 12.2626 15.0303 11.9697L14.0607 11L15.0303 10.0303C15.3232 9.73744 15.3232 9.26256 15.0303 8.96967C14.7374 8.67678 14.2626 8.67678 13.9697 8.96967L13 9.93934L12.0303 8.96967Z" fill="#1C274C"/>
|
||||||
|
<path d="M7.5 18C8.32843 18 9 18.6716 9 19.5C9 20.3284 8.32843 21 7.5 21C6.67157 21 6 20.3284 6 19.5C6 18.6716 6.67157 18 7.5 18Z" fill="#1C274C"/>
|
||||||
|
<path d="M16.5 18.0001C17.3284 18.0001 18 18.6716 18 19.5001C18 20.3285 17.3284 21.0001 16.5 21.0001C15.6716 21.0001 15 20.3285 15 19.5001C15 18.6716 15.6716 18.0001 16.5 18.0001Z" fill="#1C274C"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
7
public/cart-plus-svgrepo-com.svg
Normal file
7
public/cart-plus-svgrepo-com.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.5 18C8.32843 18 9 18.6716 9 19.5C9 20.3284 8.32843 21 7.5 21C6.67157 21 6 20.3284 6 19.5C6 18.6716 6.67157 18 7.5 18Z" fill="#1C274C"/>
|
||||||
|
<path d="M16.5 18.0001C17.3284 18.0001 18 18.6716 18 19.5001C18 20.3285 17.3284 21.0001 16.5 21.0001C15.6716 21.0001 15 20.3285 15 19.5001C15 18.6716 15.6716 18.0001 16.5 18.0001Z" fill="#1C274C"/>
|
||||||
|
<path opacity="0.5" d="M2.08368 2.7512C2.22106 2.36044 2.64921 2.15503 3.03998 2.29242L3.34138 2.39838C3.95791 2.61511 4.48154 2.79919 4.89363 3.00139C5.33426 3.21759 5.71211 3.48393 5.99629 3.89979C6.27827 4.31243 6.39468 4.76515 6.44841 5.26153C6.47247 5.48373 6.48515 5.72967 6.49184 6H17.1301C18.815 6 20.3318 6 20.7757 6.57708C21.2197 7.15417 21.0461 8.02369 20.699 9.76275L20.1992 12.1875C19.8841 13.7164 19.7266 14.4808 19.1748 14.9304C18.6231 15.38 17.8426 15.38 16.2816 15.38H10.9787C8.18979 15.38 6.79534 15.38 5.92894 14.4662C5.06254 13.5523 4.9993 12.5816 4.9993 9.64L4.9993 7.03832C4.9993 6.29837 4.99828 5.80316 4.95712 5.42295C4.91779 5.0596 4.84809 4.87818 4.75783 4.74609C4.66977 4.61723 4.5361 4.4968 4.23288 4.34802C3.91003 4.18961 3.47128 4.03406 2.80367 3.79934L2.54246 3.7075C2.1517 3.57012 1.94629 3.14197 2.08368 2.7512Z" fill="#1C274C"/>
|
||||||
|
<path d="M13.75 9C13.75 8.58579 13.4142 8.25 13 8.25C12.5858 8.25 12.25 8.58579 12.25 9V10.25H11C10.5858 10.25 10.25 10.5858 10.25 11C10.25 11.4142 10.5858 11.75 11 11.75H12.25V13C12.25 13.4142 12.5858 13.75 13 13.75C13.4142 13.75 13.75 13.4142 13.75 13V11.75H15C15.4142 11.75 15.75 11.4142 15.75 11C15.75 10.5858 15.4142 10.25 15 10.25H13.75V9Z" fill="#1C274C"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
4
public/cart-shopping-svgrepo-com.svg
Normal file
4
public/cart-shopping-svgrepo-com.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6.29977 5H21L19 12H7.37671M20 16H8L6 3H3M9 20C9 20.5523 8.55228 21 8 21C7.44772 21 7 20.5523 7 20C7 19.4477 7.44772 19 8 19C8.55228 19 9 19.4477 9 20ZM20 20C20 20.5523 19.5523 21 19 21C18.4477 21 18 20.5523 18 20C18 19.4477 18.4477 19 19 19C19.5523 19 20 19.4477 20 20Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 591 B |
@@ -105,16 +105,17 @@ function App() {
|
|||||||
if (data && data[0] && data[0].token) {
|
if (data && data[0] && data[0].token) {
|
||||||
// Update token with data[0].token
|
// Update token with data[0].token
|
||||||
document.cookie = `token=${data[0].token}; path=/`;
|
document.cookie = `token=${data[0].token}; path=/`;
|
||||||
|
|
||||||
|
const payload = parseJwt(token);
|
||||||
|
if (payload && payload.username) {
|
||||||
|
setUsername(payload.username);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('Token tidak ditemukan dalam data.');
|
console.warn('Token tidak ditemukan dalam data.');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Fetch error:', err));
|
.catch(err => console.error('Fetch error:', err));
|
||||||
|
|
||||||
const payload = parseJwt(token);
|
|
||||||
if (payload && payload.username) {
|
|
||||||
setUsername(payload.username);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
const scrollToProduct = () => {
|
const scrollToProduct = () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
width: 100%;
|
width: 40vw;
|
||||||
height: 260px;
|
height: 260px;
|
||||||
background-color: #e2e8f0;
|
background-color: #e2e8f0;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
@@ -20,6 +20,10 @@
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerRow {
|
.headerRow {
|
||||||
@@ -32,14 +36,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 1.1rem;
|
font-size: 0.9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1e293b;
|
color: #1e293b;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price {
|
.price {
|
||||||
font-size: 1.1rem;
|
font-size: 0.9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #2563eb; /* default color, bisa override di inline style */
|
color: #2563eb; /* default color, bisa override di inline style */
|
||||||
}
|
}
|
||||||
@@ -92,4 +96,41 @@
|
|||||||
.buttonGroup {
|
.buttonGroup {
|
||||||
gap: 0.5rem;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import styles from './ProductDetail.module.css';
|
|||||||
|
|
||||||
const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => {
|
const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => {
|
||||||
const [inCart, setInCart] = useState(false);
|
const [inCart, setInCart] = useState(false);
|
||||||
|
const [showChildSelector, setShowChildSelector] = useState(false);
|
||||||
|
const [selectedChildIds, setSelectedChildIds] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const existingCookie = document.cookie
|
const existingCookie = document.cookie
|
||||||
@@ -47,85 +49,193 @@ const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => {
|
|||||||
setInCart(true);
|
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 = () => {
|
const onCheckout = () => {
|
||||||
// Ambil token dari cookie
|
const tokenCookie = document.cookie
|
||||||
const tokenCookie = document.cookie
|
.split('; ')
|
||||||
.split('; ')
|
.find(row => row.startsWith('token='));
|
||||||
.find(row => row.startsWith('token='));
|
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
|
||||||
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
|
|
||||||
|
|
||||||
// Ambil itemsId dari cookie
|
if (!tokenCookie) {
|
||||||
const itemsCookie = document.cookie
|
setPostLoginAction(() => () => onCheckout());
|
||||||
.split('; ')
|
setShowedModal('login');
|
||||||
.find(row => row.startsWith('itemsId='));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let items = [];
|
// Jika punya children, tampilkan pilihan
|
||||||
if (itemsCookie) {
|
if (product.children && product.children.length > 0) {
|
||||||
try {
|
setShowChildSelector(true);
|
||||||
items = JSON.parse(itemsCookie.split('=')[1]);
|
return;
|
||||||
if (!Array.isArray(items)) items = [];
|
}
|
||||||
} catch (e) {
|
|
||||||
items = [];
|
// 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)) {
|
// Tambahkan product.id jika belum ada
|
||||||
items.push(product.id);
|
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) {
|
// Gabungkan items dari cookie dengan selectedChildIds
|
||||||
setPostLoginAction(() => () => onCheckout()); // remember intent
|
const mergedItems = Array.from(new Set([...items, ...selectedChildIds]));
|
||||||
setShowedModal('login');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect dengan token dan itemsId di query route ke checkout.kediritechnopark.com
|
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`;
|
|
||||||
};
|
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';
|
const priceColor = product.price === 0 ? '#059669' : '#2563eb';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.image}>📦</div>
|
|
||||||
|
|
||||||
<div className={styles.headerRow}>
|
{/* ✅ Tampilan utama disembunyikan jika sedang memilih child */}
|
||||||
<h2 className={styles.title}>{product.name}</h2>
|
{!showChildSelector && (
|
||||||
<div className={styles.price} style={{ color: priceColor }}>
|
<>
|
||||||
{product.price == null
|
<div
|
||||||
? 'Pay-As-You-Go'
|
className={styles.image}
|
||||||
: `Rp ${parseInt(product.price).toLocaleString('id-ID')}`}
|
style={{ backgroundImage: `url(${product.image})` }}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div className={styles.headerRow}>
|
||||||
|
<h2 className={styles.title}>{product.name}</h2>
|
||||||
|
<div className={styles.price} style={{ color: priceColor }}>
|
||||||
|
{product.price == null
|
||||||
|
? 'Pay-As-You-Go'
|
||||||
|
: `Rp ${parseInt(product.price).toLocaleString('id-ID')}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className={styles.description}>{product.description}</p>
|
||||||
|
|
||||||
|
<div className={styles.buttonGroup}>
|
||||||
|
<button
|
||||||
|
className={`${styles.button} ${styles.addToCartButton}`}
|
||||||
|
onClick={onSetCart}
|
||||||
|
onMouseOver={e => (e.target.style.backgroundColor = '#facc15')}
|
||||||
|
onMouseOut={e => (e.target.style.backgroundColor = '#fbbf24')}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={'/cart-shopping-svgrepo-com.svg'}
|
||||||
|
alt={inCart ? 'Hapus' : 'Tambah'}
|
||||||
|
style={{ width: '21px', height: '21px', marginRight: '7px' }}
|
||||||
|
/>
|
||||||
|
{inCart ? 'Hapus' : 'Tambah'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`${styles.button} ${styles.checkoutButton}`}
|
||||||
|
onClick={onCheckout}
|
||||||
|
onMouseOver={e => (e.target.style.backgroundColor = '#1d4ed8')}
|
||||||
|
onMouseOut={e => (e.target.style.backgroundColor = '#2563eb')}
|
||||||
|
>
|
||||||
|
Checkout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ✅ UI pemilihan child */}
|
||||||
|
{showChildSelector && (
|
||||||
|
<div className={styles.childSelector}>
|
||||||
|
<h3>Pilih Paket</h3>
|
||||||
|
{product.children.map(child => (
|
||||||
|
<label key={child.id} className={styles.childProduct} style={{ display: 'block', marginBottom: '8px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={child.id}
|
||||||
|
checked={selectedChildIds.includes(child.id)}
|
||||||
|
onChange={e => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
if (checked) {
|
||||||
|
setSelectedChildIds(prev => [...prev, child.id]);
|
||||||
|
} else {
|
||||||
|
setSelectedChildIds(prev => prev.filter(id => id !== child.id));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{' '}
|
||||||
|
{child.name} — Rp {parseInt(child.price || 0).toLocaleString('id-ID')}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<p style={{ marginTop: '10px' }}>
|
||||||
|
<strong>Total Harga:</strong>{' '}
|
||||||
|
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')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={styles.buttonGroup}>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`${styles.button} ${styles.cancelButton}`}
|
||||||
|
onClick={() => {
|
||||||
|
setShowChildSelector(false);
|
||||||
|
setSelectedChildIds([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.button} ${styles.confirmButton}`}
|
||||||
|
onClick={onConfirmChildren}
|
||||||
|
>
|
||||||
|
Lanjut ke Checkout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<p className={styles.description}>{product.description}</p>
|
|
||||||
|
|
||||||
<div className={styles.buttonGroup}>
|
|
||||||
<button
|
|
||||||
className={`${styles.button} ${styles.addToCartButton}`}
|
|
||||||
onClick={onSetCart}
|
|
||||||
onMouseOver={e => (e.target.style.backgroundColor = '#facc15')}
|
|
||||||
onMouseOut={e => (e.target.style.backgroundColor = '#fbbf24')}
|
|
||||||
>
|
|
||||||
{inCart ? 'Hapus dari Keranjang' : '+ Keranjang'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`${styles.button} ${styles.checkoutButton}`}
|
|
||||||
onClick={onCheckout}
|
|
||||||
onMouseOver={e => (e.target.style.backgroundColor = '#1d4ed8')}
|
|
||||||
onMouseOut={e => (e.target.style.backgroundColor = '#2563eb')}
|
|
||||||
>
|
|
||||||
Checkout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,18 +6,48 @@ import styles from './Styles.module.css';
|
|||||||
const ProductSection = ({ hoveredCard, setHoveredCard, setSelectedProduct, setShowedModal, productSectionRef }) => {
|
const ProductSection = ({ hoveredCard, setHoveredCard, setSelectedProduct, setShowedModal, productSectionRef }) => {
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
|
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ type: 'product', onlyParents: true }),
|
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())
|
.catch(err => console.error('Fetch error:', err));
|
||||||
.then(data => setProducts(data))
|
}, []);
|
||||||
.catch(err => console.error('Fetch error:', err));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
@@ -31,7 +61,8 @@ const ProductSection = ({ hoveredCard, setHoveredCard, setSelectedProduct, setSh
|
|||||||
<div className={styles.coursesGrid}>
|
<div className={styles.coursesGrid}>
|
||||||
{products &&
|
{products &&
|
||||||
products[0]?.name &&
|
products[0]?.name &&
|
||||||
products.map(product => (
|
products
|
||||||
|
.map(product => (
|
||||||
<div
|
<div
|
||||||
key={product.id}
|
key={product.id}
|
||||||
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
|
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import ProductDetailPage from '../ProductDetailPage';
|
|||||||
import Login from '../Login';
|
import Login from '../Login';
|
||||||
import styles from '../Styles.module.css';
|
import styles from '../Styles.module.css';
|
||||||
|
|
||||||
// Fungsi simple untuk parsing token JWT dan mengembalikan payload JSON
|
|
||||||
function parseJwt(token) {
|
function parseJwt(token) {
|
||||||
try {
|
try {
|
||||||
const base64Url = token.split('.')[1];
|
const base64Url = token.split('.')[1];
|
||||||
@@ -20,32 +20,122 @@ function parseJwt(token) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDistinctProductIdsFromJwt(token) {
|
||||||
|
const payload = parseJwt(token);
|
||||||
|
if (!payload || !payload.subscriptions || !payload.subscriptions) return [];
|
||||||
|
|
||||||
|
const productIds = payload.subscriptions.map(p => 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 CoursePage = () => {
|
||||||
const [postLoginAction, setPostLoginAction] = useState(null);
|
const [postLoginAction, setPostLoginAction] = useState(null);
|
||||||
const [selectedProduct, setSelectedProduct] = useState({});
|
const [selectedProduct, setSelectedProduct] = useState({});
|
||||||
const [hoveredCard, setHoveredCard] = useState(null);
|
const [hoveredCard, setHoveredCard] = useState(null);
|
||||||
const [showedModal, setShowedModal] = useState(null); // 'product' | 'login' | null
|
const [showedModal, setShowedModal] = useState(null);
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
||||||
|
if (match) {
|
||||||
|
const token = match[2];
|
||||||
|
|
||||||
useEffect(() => {
|
const productIds = getDistinctProductIdsFromJwt(token);
|
||||||
// Ambil token dari cookies
|
const endDates = getLatestEndDatesFromJwt(token);
|
||||||
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
const tokenQuantitiesFromJwt = getTotalTokenFromJwt(token);
|
||||||
if (match) {
|
|
||||||
const token = match[2];
|
|
||||||
|
|
||||||
fetch('https://bot.kediritechnopark.com/webhook/users-dev/my-products', {
|
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer ' + token
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ type: 'product' }),
|
body: JSON.stringify({ itemsId: productIds, type: 'product' }),
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => setProducts(data))
|
.then(data => {
|
||||||
.catch(err => console.error('Fetch error:', err));
|
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 = [
|
const features = [
|
||||||
{
|
{
|
||||||
@@ -70,49 +160,52 @@ const CoursePage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ fontFamily: 'Inter, system-ui, sans-serif' }}>
|
<div style={{ fontFamily: 'Inter, system-ui, sans-serif' }}>
|
||||||
|
|
||||||
{/* Courses Section */}
|
{/* Courses Section */}
|
||||||
<section className={styles.Section}>
|
<section className={styles.Section}>
|
||||||
<div className={styles.coursesContainer}>
|
<div className={styles.coursesContainer}>
|
||||||
<h2 className={styles.coursesTitle}>OUR COURSES</h2>
|
<h2 className={styles.coursesTitle}>MY PRODUCTS</h2>
|
||||||
<div className={styles.coursesGrid}>
|
<div className={styles.coursesGrid}>
|
||||||
{products &&
|
{products &&
|
||||||
products[0]?.name &&
|
products[0]?.name &&
|
||||||
products.map(product => (
|
products
|
||||||
<div
|
.map(product => (
|
||||||
key={product.id}
|
<div
|
||||||
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
|
key={product.id}
|
||||||
onClick={() => {
|
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
|
||||||
setSelectedProduct(product);
|
onClick={() => {
|
||||||
setShowedModal('product');
|
setSelectedProduct(product);
|
||||||
}}
|
setShowedModal('product');
|
||||||
onMouseEnter={() => setHoveredCard(product.id)}
|
}}
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
onMouseEnter={() => setHoveredCard(product.id)}
|
||||||
>
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
<div className={styles.courseImage}>
|
>
|
||||||
{product.price == 0 && (
|
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
|
||||||
<span className={styles.courseLabel}>Free</span>
|
{product.price == 0 && (
|
||||||
)}
|
<span className={styles.courseLabel}>Free</span>
|
||||||
</div>
|
)}
|
||||||
<div className={styles.courseContent}>
|
</div>
|
||||||
<h3 className={styles.courseTitle}>{product.name}</h3>
|
<div className={styles.courseContent}>
|
||||||
<p className={styles.courseDesc}>{product.description}</p>
|
<h3 className={styles.courseTitle}>{product.name}</h3>
|
||||||
<div className={styles.coursePrice}>
|
<p className={styles.courseDesc}>{product.description}</p>
|
||||||
<span
|
<div className={styles.coursePrice}>
|
||||||
className={
|
<span
|
||||||
product.price == 0
|
className={
|
||||||
? styles.freePrice
|
product.price == 0
|
||||||
: styles.currentPrice
|
? styles.freePrice
|
||||||
}
|
: styles.currentPrice
|
||||||
>
|
}
|
||||||
{product.price == 0
|
>
|
||||||
? 'Free'
|
{product.unit_type === 'duration'
|
||||||
: `Rp ${product.price.toLocaleString('id-ID')}`}
|
? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : 'N/A'}`
|
||||||
</span>
|
: `SISA TOKEN ${product.quantity || 0}`
|
||||||
|
}
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user