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

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

View 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

View 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

View File

@@ -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 = () => {

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,17 +49,27 @@ 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 onCheckout = () => {
const tokenCookie = document.cookie
.split('; ')
.find(row => row.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
if (!tokenCookie) {
setPostLoginAction(() => () => onCheckout());
setShowedModal('login');
return;
}
// Jika punya children, tampilkan pilihan
if (product.children && product.children.length > 0) {
setShowChildSelector(true);
return;
}
// Ambil itemsId dari cookie
const itemsCookie = document.cookie
.split('; ')
@@ -73,29 +85,63 @@ const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => {
}
}
// Tambahkan product.id jika belum ada
if (!items.includes(product.id)) {
items.push(product.id);
}
// Encode items ke string untuk query param
const itemsParam = JSON.stringify(items);
if (!tokenCookie) {
setPostLoginAction(() => () => onCheckout()); // remember intent
setShowedModal('login');
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;
}
// Redirect dengan token dan itemsId di query route ke checkout.kediritechnopark.com
// 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 = [];
}
}
// Gabungkan items dari cookie dengan selectedChildIds
const mergedItems = Array.from(new Set([...items, ...selectedChildIds]));
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>
{/* ✅ 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>
@@ -115,8 +161,14 @@ const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => {
onMouseOver={e => (e.target.style.backgroundColor = '#facc15')}
onMouseOut={e => (e.target.style.backgroundColor = '#fbbf24')}
>
{inCart ? 'Hapus dari Keranjang' : '+ Keranjang'}
<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}
@@ -126,6 +178,64 @@ const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => {
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>
);
};

View File

@@ -6,18 +6,48 @@ import styles from './Styles.module.css';
const ProductSection = ({ hoveredCard, setHoveredCard, setSelectedProduct, setShowedModal, productSectionRef }) => {
const [products, setProducts] = useState([]);
useEffect(() => {
useEffect(() => {
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ type: 'product', onlyParents: true }),
body: JSON.stringify({ type: 'product' }),
})
.then(res => res.json())
.then(data => setProducts(data))
.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);
})
.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(() => {
// Ambil token dari cookies
useEffect(() => {
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
if (match) {
const token = match[2];
fetch('https://bot.kediritechnopark.com/webhook/users-dev/my-products', {
const productIds = getDistinctProductIdsFromJwt(token);
const endDates = getLatestEndDatesFromJwt(token);
const tokenQuantitiesFromJwt = getTotalTokenFromJwt(token);
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))
.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 = [
{
@@ -74,11 +164,12 @@ const CoursePage = () => {
{/* 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 => (
products
.map(product => (
<div
key={product.id}
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
@@ -89,7 +180,7 @@ const CoursePage = () => {
onMouseEnter={() => setHoveredCard(product.id)}
onMouseLeave={() => setHoveredCard(null)}
>
<div className={styles.courseImage}>
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
{product.price == 0 && (
<span className={styles.courseLabel}>Free</span>
)}
@@ -105,9 +196,11 @@ const CoursePage = () => {
: styles.currentPrice
}
>
{product.price == 0
? 'Free'
: `Rp ${product.price.toLocaleString('id-ID')}`}
{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>