This commit is contained in:
Vassshhh
2025-08-01 21:14:12 +07:00
parent 7a0a0983dd
commit 690bb837f6
8 changed files with 423 additions and 129 deletions

View File

@@ -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;
}

View File

@@ -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 (
<div className={styles.container}>
<div className={styles.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')}`}
{/* ✅ Tampilan utama disembunyikan jika sedang memilih child */}
{!showChildSelector && (
<>
<div
className={styles.image}
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>
<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>
);
};

View File

@@ -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
<div className={styles.coursesGrid}>
{products &&
products[0]?.name &&
products.map(product => (
products
.map(product => (
<div
key={product.id}
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}

View File

@@ -3,7 +3,7 @@ import ProductDetailPage from '../ProductDetailPage';
import Login from '../Login';
import styles from '../Styles.module.css';
// Fungsi simple untuk parsing token JWT dan mengembalikan payload JSON
function parseJwt(token) {
try {
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 [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 (
<div style={{ fontFamily: 'Inter, system-ui, sans-serif' }}>
{/* Courses Section */}
<section className={styles.Section}>
<div className={styles.coursesContainer}>
<h2 className={styles.coursesTitle}>OUR COURSES</h2>
<h2 className={styles.coursesTitle}>MY PRODUCTS</h2>
<div className={styles.coursesGrid}>
{products &&
products[0]?.name &&
products.map(product => (
<div
key={product.id}
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
onClick={() => {
setSelectedProduct(product);
setShowedModal('product');
}}
onMouseEnter={() => setHoveredCard(product.id)}
onMouseLeave={() => setHoveredCard(null)}
>
<div className={styles.courseImage}>
{product.price == 0 && (
<span className={styles.courseLabel}>Free</span>
)}
</div>
<div className={styles.courseContent}>
<h3 className={styles.courseTitle}>{product.name}</h3>
<p className={styles.courseDesc}>{product.description}</p>
<div className={styles.coursePrice}>
<span
className={
product.price == 0
? styles.freePrice
: styles.currentPrice
}
>
{product.price == 0
? 'Free'
: `Rp ${product.price.toLocaleString('id-ID')}`}
</span>
products
.map(product => (
<div
key={product.id}
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
onClick={() => {
setSelectedProduct(product);
setShowedModal('product');
}}
onMouseEnter={() => setHoveredCard(product.id)}
onMouseLeave={() => setHoveredCard(null)}
>
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
{product.price == 0 && (
<span className={styles.courseLabel}>Free</span>
)}
</div>
<div className={styles.courseContent}>
<h3 className={styles.courseTitle}>{product.name}</h3>
<p className={styles.courseDesc}>{product.description}</p>
<div className={styles.coursePrice}>
<span
className={
product.price == 0
? styles.freePrice
: styles.currentPrice
}
>
{product.unit_type === 'duration'
? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : 'N/A'}`
: `SISA TOKEN ${product.quantity || 0}`
}
</span>
</div>
</div>
</div>
</div>
))}
))}
</div>
</div>
</section>