Compare commits

...

3 Commits

Author SHA1 Message Date
everythingonblack
e3154e4cde ok 2025-08-18 01:36:43 +07:00
everythingonblack
7add8f2864 Merge remote-tracking branch 'kediritechnopark.com/main' 2025-08-18 00:06:04 +07:00
everythingonblack
361ba6316b ok 2025-08-17 23:59:18 +07:00
6 changed files with 632 additions and 287 deletions

View File

@@ -10,9 +10,9 @@ import ProductSection from './components/ProductSection';
import AcademySection from './components/AcademySection'; import AcademySection from './components/AcademySection';
import AboutUsSection from './components/AboutUsSection'; import AboutUsSection from './components/AboutUsSection';
// KnowledgeBaseSection hidden temporarily // KnowledgeBaseSection hidden temporarily
// import KnowledgeBaseSection from './components/KnowledgeBaseSection'; import KnowledgeBaseSection from './components/KnowledgeBaseSection';
// ClientsSection hidden temporarily // ClientsSection hidden temporarily
// import ClientsSection from './components/ClientsSection'; import ClientsSection from './components/ClientsSection';
import FAQSection from './components/FAQSection'; import FAQSection from './components/FAQSection';
import Footer from './components/Footer'; import Footer from './components/Footer';
import ProductDetailPage from './components/ProductDetailPage'; import ProductDetailPage from './components/ProductDetailPage';
@@ -27,8 +27,19 @@ function HomePage({
setShowedModal, setShowedModal,
productSectionRef, productSectionRef,
courseSectionRef, courseSectionRef,
scrollToProduct,
scrollToCourse,
setWillDo setWillDo
}) { }) {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const tab = params.get('tab');
if (tab === 'products') scrollToProduct();
if (tab === 'academy') scrollToCourse();
}, [productSectionRef, courseSectionRef]);
return ( return (
<> <>
<HeroSection /> <HeroSection />
@@ -47,9 +58,9 @@ function HomePage({
setShowedModal={setShowedModal} setShowedModal={setShowedModal}
setWillDo={setWillDo} setWillDo={setWillDo}
/> />
{/* <KnowledgeBaseSection /> */} <KnowledgeBaseSection />
{/* <ClientsSection /> */} <ClientsSection />
<FAQSection /> <Footer />
</> </>
); );
} }
@@ -134,6 +145,7 @@ function App() {
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const modalType = params.get('modal'); const modalType = params.get('modal');
const tab = params.get('tab');
const productId = params.get('product_id'); const productId = params.get('product_id');
const authorizedUri = params.get('authorized_uri'); const authorizedUri = params.get('authorized_uri');
const unauthorizedUri = params.get('unauthorized_uri'); const unauthorizedUri = params.get('unauthorized_uri');
@@ -152,6 +164,9 @@ function App() {
} }
// Jika sudah login, tidak langsung fetch di sini — akan diproses saat subscriptions tersedia // Jika sudah login, tidak langsung fetch di sini — akan diproses saat subscriptions tersedia
} }
if (tab === 'products') scrollToProduct();
if (tab === 'academy') scrollToCourse();
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -303,12 +318,14 @@ function App() {
productSectionRef={productSectionRef} productSectionRef={productSectionRef}
courseSectionRef={courseSectionRef} courseSectionRef={courseSectionRef}
setWillDo={setWillDo} setWillDo={setWillDo}
scrollToProduct={scrollToProduct}
scrollToCourse={scrollToCourse}
/> />
} }
/> />
<Route path="/dashboard" element={<ProductsPage <Route path="/dashboard" element={<ProductsPage
setShowedModal={setShowedModal} setShowedModal={setShowedModal}
setSelectedProduct={setSelectedProduct} subscriptions={subscriptions} />} /> setSelectedProduct={setSelectedProduct} subscriptions={subscriptions} setWillDo={setWillDo} />} />
<Route <Route
path="/admin" path="/admin"
element={ element={
@@ -321,7 +338,6 @@ function App() {
} }
/> />
</Routes> </Routes>
<Footer />
{/* Modal */} {/* Modal */}
{showedModal && ( {showedModal && (

View File

@@ -105,6 +105,10 @@ const LoginRegister = ({setShowedModal}) => {
window.history.replaceState({}, '', newUrl); window.history.replaceState({}, '', newUrl);
setShowedModal('product'); setShowedModal('product');
} else { } else {
const params = new URLSearchParams(window.location.search);
const modalType = params.get('modal');
if(!modalType)
navigate('/dashboard'); navigate('/dashboard');
window.location.reload(); window.location.reload();
} }

View File

@@ -1,17 +1,20 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import styles from './ProductDetail.module.css'; import styles from './ProductDetail.module.css';
import { useNavigate } from 'react-router-dom';
const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin, setShowedModal }) => { const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin, setShowedModal }) => {
const [showChildSelector, setShowChildSelector] = useState(false); const [showChildSelector, setShowChildSelector] = useState(false);
const [selectedChildIds, setSelectedChildIds] = useState([]); const [selectedChildIds, setSelectedChildIds] = useState([]);
const [matchingSubscriptions, setMatchingSubscriptions] = useState([]); const [matchingSubscriptions, setMatchingSubscriptions] = useState([]);
const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(null); const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(0);
const [showSubscriptionSelector, setShowSubscriptionSelector] = useState(false); const [showSubscriptionSelector, setShowSubscriptionSelector] = useState(false);
const [showNamingInput, setShowNamingInput] = useState(false); const [showNamingInput, setShowNamingInput] = useState(false);
const [customName, setCustomName] = useState(''); const [customName, setCustomName] = useState('');
const navigate = useNavigate();
const onCheckout = () => { const onCheckout = () => {
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token=')); const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : ''; const token = tokenCookie ? tokenCookie.split('=')[1] : '';
@@ -20,34 +23,14 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
requestLogin('checkout'); requestLogin('checkout');
return; return;
} }
if (product.type == 'product') {
if (product.type === 'product') {
const hasMatchingSubscription = Array.isArray(subscriptions) && const hasMatchingSubscription = Array.isArray(subscriptions) &&
subscriptions.some(sub => subscriptions.some(sub =>
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id) String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
); );
// Always show children selector first if product has children // ✅ Check subscription first
if (product.children && product.children.length > 0) {
setShowChildSelector(true);
if (hasMatchingSubscription) {
const matching = subscriptions.filter(sub =>
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
);
if (matching.length > 0) {
// ✅ Select only the first for each product_name
const uniqueByName = Array.from(
new Map(matching.map(sub => [sub.product_name, sub])).values()
);
setMatchingSubscriptions(uniqueByName);
}
}
return;
}
// No children, but has subscription match
if (hasMatchingSubscription) { if (hasMatchingSubscription) {
const matching = subscriptions.filter(sub => const matching = subscriptions.filter(sub =>
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id) String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
@@ -61,56 +44,59 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
setMatchingSubscriptions(uniqueByName); setMatchingSubscriptions(uniqueByName);
setShowSubscriptionSelector(true); setShowSubscriptionSelector(true);
return; return;
} } else {
else {
const itemsParam = JSON.stringify([product.id]); const itemsParam = JSON.stringify([product.id]);
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${product.name}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`; window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${product.name}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
return; return;
} }
} }
// ✅ If no subscription → ask for new product name
setShowNamingInput(true); setShowNamingInput(true);
return; return;
} }
// No children, no matching subscription
// Fallback: direct checkout
const itemsParam = JSON.stringify([product.id]); const itemsParam = JSON.stringify([product.id]);
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`; window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
}; };
// ✅ Confirm child selection (final step after naming)
const onConfirmChildren = () => { const onConfirmChildren = () => {
if (matchingSubscriptions.length > 0) { if (selectedChildIds.length === 0) {
setShowChildSelector(false); alert('Pilih minimal satu produk');
setShowSubscriptionSelector(true);
return;
}
else {
setShowChildSelector(false);
setShowNamingInput(true);
return; return;
} }
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token=')); const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : ''; const token = tokenCookie ? tokenCookie.split('=')[1] : '';
if (selectedChildIds.length === 0) { const encodedName = encodeURIComponent(customName.trim() || product.name);
alert('Pilih minimal satu produk'); const itemsParam = JSON.stringify(selectedChildIds);
return;
}
const itemsParam = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : JSON.stringify([product.id]); window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
}; };
// ✅ User sets name first → then if product has children, show child selector
const onFinalCheckoutNewProduct = () => { const onFinalCheckoutNewProduct = () => {
if (!customName.trim()) { if (!customName.trim()) {
alert('Nama produk tidak boleh kosong'); alert('Nama produk tidak boleh kosong');
return; return;
} }
if (product.children && product.children.length > 0) {
// dont redirect yet → go to child selector
setShowSubscriptionSelector(false);
setShowNamingInput(false);
setShowChildSelector(true);
return;
}
// if no children → go straight to checkout
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token=')); const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : ''; const token = tokenCookie ? tokenCookie.split('=')[1] : '';
const itemsParam = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : JSON.stringify([product.id]); const itemsParam = JSON.stringify([product.id]);
const encodedName = encodeURIComponent(customName.trim()); const encodedName = encodeURIComponent(customName.trim());
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`; window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
@@ -143,13 +129,14 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
if (willDo === 'checkout') { if (willDo === 'checkout') {
onCheckout(); onCheckout();
} }
if(setWillDo) setWillDo(''); // Reset willDo after handling if (setWillDo) setWillDo('');
}, []); }, []);
const priceColor = product.price === 0 ? '#059669' : '#2563eb'; const priceColor = product.price === 0 ? '#059669' : '#2563eb';
console.log(product)
return ( return (
<div className={styles.container}> <div className={styles.container}>
{/* Default view */}
{!showChildSelector && !showSubscriptionSelector && !showNamingInput && ( {!showChildSelector && !showSubscriptionSelector && !showNamingInput && (
<> <>
<div className={styles.image} style={{ backgroundImage: `url(${product.image})` }}></div> <div className={styles.image} style={{ backgroundImage: `url(${product.image})` }}></div>
@@ -182,38 +169,30 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
sub.product_id === product.id || sub.product_parent_id === product.id sub.product_id === product.id || sub.product_parent_id === product.id
) && product.end_date ? 'Perpanjang' : 'Checkout'} ) && product.end_date ? 'Perpanjang' : 'Checkout'}
</button> </button>
</div> </div>
</> </>
)} )}
{/* Child selector */}
{showChildSelector && ( {showChildSelector && (
<div className={styles.childSelector}> <div className={styles.childSelector}>
<h3>Pilih Paket</h3> <h3>Pilih Paket</h3>
{product.children.map(child => ( {product.children.map(child => (
<label key={child.id} className={styles.childProduct}> <label key={child.id} className={styles.childProduct}>
<input <input
type="checkbox" type="radio"
value={child.id} value={child.id}
checked={selectedChildIds.includes(child.id)} checked={selectedChildIds.includes(child.id)}
onChange={e => { onChange={() => setSelectedChildIds([child.id])}
const checked = e.target.checked;
setSelectedChildIds(prev =>
checked ? [...prev, child.id] : prev.filter(id => id !== child.id)
);
}}
/> />
&nbsp;{child.name} Rp {parseInt(child.price || 0).toLocaleString('id-ID')} <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>&nbsp;{child.name}</div>
<div>Rp {parseInt(child.price || 0).toLocaleString('id-ID')}</div>
</div>
</label> </label>
))} ))}
<p>
<strong>Total Harga:</strong> Rp {selectedChildIds
.map(id => product.children.find(child => child.id === id)?.price || 0)
.reduce((a, b) => a + b, 0)
.toLocaleString('id-ID')}
</p>
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<button className={styles.button} onClick={() => setShowChildSelector(false)}> <button className={styles.button} onClick={() => { setShowChildSelector(false); setShowNamingInput(true); }}>
Kembali Kembali
</button> </button>
<button className={styles.button} onClick={onConfirmChildren}> <button className={styles.button} onClick={onConfirmChildren}>
@@ -223,42 +202,32 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
</div> </div>
)} )}
{/* Subscription selector */}
{showSubscriptionSelector && !showNamingInput && ( {showSubscriptionSelector && !showNamingInput && (
<div className={styles.childSelector}> <div className={styles.childSelector}>
<h5>Perpanjang {product.name.split('%%%')[0]} </h5> <h5>Kamu sudah punya produk ini</h5>
{matchingSubscriptions.map(sub => ( <div className={styles.childProduct} onClick={() => { setShowedModal(''); navigate('/dashboard') }}>
<label key={sub.id} className={styles.childProduct}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<input <div>Perpanjang produk ini</div>
type="radio" <div></div>
name="subscription" </div>
value={sub.id} </div>
checked={selectedSubscriptionId == sub.id} <h6>Atau</h6>
onChange={() => { setSelectedSubscriptionId(sub.id); setCustomName(sub.product_name) }} <label className={styles.childProduct} onClick={() => { setSelectedSubscriptionId(0); onConfirmSelector(); }}>
/> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
&nbsp;{sub.product_name.split('%%%')[0]} <div>Tambah {product.name.split('%%%')[0]} baru</div>
</label> <div></div>
))} </div>
<h6>Atau buat baru</h6>
<label className={styles.childProduct}>
<input
type="radio"
name="subscription"
checked={selectedSubscriptionId === 0}
onChange={() => {setSelectedSubscriptionId(0); console.log(product.id)}}
/>
&nbsp;Buat {product.name.split('%%%')[0]} baru
</label> </label>
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<button className={styles.button} onClick={() => setShowSubscriptionSelector(false)}> <button className={styles.button} onClick={() => setShowSubscriptionSelector(false)}>
Kembali Kembali
</button> </button>
<button className={styles.button} onClick={onConfirmSelector}>
Lanjut ke Checkout
</button>
</div> </div>
</div> </div>
)} )}
{/* Naming input */}
{showNamingInput && ( {showNamingInput && (
<div className={styles.childSelector}> <div className={styles.childSelector}>
<h5>Buat {product.name.split('%%%')[0]} Baru</h5> <h5>Buat {product.name.split('%%%')[0]} Baru</h5>
@@ -271,47 +240,20 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
style={{ width: '100%', padding: '8px', marginBottom: '16px', borderRadius: '10px' }} style={{ width: '100%', padding: '8px', marginBottom: '16px', borderRadius: '10px' }}
/> />
{
matchingSubscriptions.some(
(sub) => sub.product_name === `${product.name}@${customName}`
) && (
<p style={{ color: 'red', marginBottom: '10px' }}>
Nama produk sudah digunakan.
</p>
)
}
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<button <button className={styles.button} onClick={() => setShowNamingInput(false)}>
className={styles.button}
onClick={() => {
setShowNamingInput(false);
const hasMatchingSubscription = Array.isArray(subscriptions) &&
subscriptions.some(sub =>
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
);
if (hasMatchingSubscription) setShowSubscriptionSelector(true);
}}
>
Kembali Kembali
</button> </button>
<button <button
className={styles.button} className={styles.button}
onClick={onFinalCheckoutNewProduct} onClick={onFinalCheckoutNewProduct}
disabled={ disabled={customName.trim() === ''}
customName.trim() === '' ||
matchingSubscriptions.some(
(sub) => sub.product_name === `${product.name}@${customName}`
)
}
> >
Checkout Lanjut
</button> </button>
</div> </div>
</div> </div>
)} )}
</div> </div>
); );
}; };

View File

@@ -91,7 +91,6 @@
.carouselContainer { .carouselContainer {
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
padding: 0 56px;
min-height: 380px; /* compact */ min-height: 380px; /* compact */
} }
@@ -133,7 +132,6 @@
} }
.carouselContainer { .carouselContainer {
padding: 0 60px;
min-height: 420px; min-height: 420px;
} }
@@ -167,7 +165,6 @@
} }
.carouselContainer { .carouselContainer {
padding: 0 50px;
min-height: 400px; min-height: 400px;
} }
@@ -210,7 +207,6 @@
} }
.carouselContainer { .carouselContainer {
padding: 0 40px;
min-height: 370px; min-height: 370px;
} }
@@ -253,7 +249,6 @@
} }
.carouselContainer { .carouselContainer {
padding: 0 30px;
min-height: 320px; min-height: 320px;
} }
@@ -296,7 +291,6 @@
} }
.carouselContainer { .carouselContainer {
padding: 0 25px;
min-height: 300px; min-height: 300px;
} }

View File

@@ -421,7 +421,7 @@
} }
.currentPrice { .currentPrice {
font-size: 0.95rem; font-size: 1.2rem;
font-weight: bold; font-weight: bold;
color: #2563eb; color: #2563eb;
} }
@@ -950,3 +950,139 @@
max-width: 150px; /* biar logo tidak terlalu besar */ max-width: 150px; /* biar logo tidak terlalu besar */
height: auto; height: auto;
} }
/* Navigation Tabs */
.navTabs {
display: flex;
align-items: center;
background-color: #2a4fd6; /* blue bar like screenshot */
padding: 0;
border-radius: 6px 6px 0 0;
overflow: hidden;
}
.floatMenuItem {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 12px 16px;
font-size: 14px;
font-weight: 500;
color: #ffffffcc; /* light white text */
background: transparent;
border: none;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
.floatMenuItem:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
.floatMenuItemActive {
background-color: #ffffff;
color: #2a4fd6; /* active text color */
font-weight: 600;
}
.floatMenuTitle {
display: inline-block;
}
.floatMenuIcon {
stroke-width: 2;
}
/* Sections */
.Section {
padding: 24px;
background: #fff;
border-radius: 0 0 6px 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* Settings Page Layout */
.profileSection {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.profileHeading {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.sectionDivider {
height: 2px;
background-color: #2a4fd6;
width: 40px;
margin: 8px 0 20px 0;
}
.formGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
background-color: #f6f9fc;
padding: 20px;
border-radius: 8px;
}
.fullWidth {
grid-column: span 2;
}
.label {
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 6px;
display: block;
}
.inputField,
.selectField {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
background-color: #fff;
font-size: 14px;
}
.selectField {
appearance: none;
}
.saveButton {
background-color: #2a4fd6;
color: white;
font-size: 14px;
font-weight: 500;
padding: 10px 24px;
border-radius: 50px;
border: none;
cursor: pointer;
margin-top: 16px;
transition: background 0.2s ease;
}
.saveButton:hover {
background-color: #223fa9;
}
/* Change Password Section */
.changePasswordSection {
margin-top: 32px;
}
.buttonRow {
display: flex;
gap: 8px;
}

View File

@@ -1,172 +1,425 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import ProductDetailPage from '../ProductDetailPage'; import styles from "../Styles.module.css";
import Login from '../Login'; import { Box, Settings, ShoppingCart } from "lucide-react";
import styles from '../Styles.module.css'; import { useNavigate } from 'react-router-dom';
const CoursePage = ({ subscriptions, setSelectedProduct, setShowedModal}) => {
const [hoveredCard, setHoveredCard] = useState(null);
const [products, setProducts] = useState([]);
// Buka modal otomatis berdasarkan query const Dashboard = ({
useEffect(() => { subscriptions,
const urlParams = new URLSearchParams(window.location.search); setSelectedProduct,
const modal = urlParams.get('modal'); setShowedModal,
const productId = urlParams.get('product_id'); userData,
setWillDo
}) => {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("products");
const [hoveredCard, setHoveredCard] = useState(null);
const [products, setProducts] = useState([]);
if (modal === 'product' && productId && products.length > 0) { // User Settings form state
const product = products.find(p => String(p.id) === productId); const [settings, setSettings] = useState({
if (product) { username: "",
setSelectedProduct(product); email: "",
setShowedModal('product'); password: "",
profile_data: {
name: "",
image: "",
phone: "",
address: "",
company: ""
}
});
useEffect(() => {
if (userData) {
setSettings(userData);
}
}, [userData]);
useEffect(() => {
if (!subscriptions) return;
function groupSubscriptionsByProductName(subs) {
const result = {};
subs.forEach((sub) => {
const name = sub.product_name;
const productId = sub.product_id;
if (!result[name]) {
result[name] = {
product_id: productId,
product_name: name,
unit_type: sub.unit_type,
end_date: sub.end_date,
quantity: 0,
subscriptions: []
};
}
const currentEnd = new Date(result[name].end_date);
const thisEnd = new Date(sub.end_date);
if (thisEnd > currentEnd) {
result[name].end_date = sub.end_date;
}
if (sub.unit_type === "token") {
result[name].quantity += sub.quantity ?? 0;
} else {
result[name].quantity += 1;
}
result[name].subscriptions.push(sub);
});
return result;
}
const groupedSubs = groupSubscriptionsByProductName(subscriptions);
const productIds = [...new Set(subscriptions.map((s) => s.product_id))];
fetch(
"https://bot.kediritechnopark.com/webhook/store-production/products",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ itemsId: productIds})
}
)
.then((res) => res.json())
.then((data) => {
// Step 1: Enrich base products (without children yet)
const enrichedData = Object.values(groupedSubs)
.filter((group) => data.some((p) => p.id === group.product_id))
.map((group) => {
const productData = data.find((p) => p.id == group.product_id);
let image = productData?.image || "";
let description = productData?.description || "";
let site_url = productData?.site_url || "";
if (!image && productData?.sub_product_of) {
const parent = data.find(
(p) => p.id === productData.sub_product_of
);
image = parent?.image || "";
description = parent?.description || "";
site_url = parent?.site_url || "";
} }
} return {
}, [products]); executeCheckout: group.product_name,
id: group.product_id,
name: group.product_name,
type: productData?.type || "product",
image,
description,
site_url,
price: productData?.price || 0,
currency: productData?.currency || "IDR",
duration: productData?.duration || {},
sub_product_of: productData?.sub_product_of || null,
is_visible: productData?.is_visible ?? true,
unit_type: productData?.unit_type || group.unit_type,
quantity: group.quantity,
end_date: group.end_date,
children: []
};
});
useEffect(() => { // Step 2: Create a quick lookup table for enrichedData
if (!subscriptions) return; const productMap = {};
enrichedData.forEach((p) => {
console.log(p)
productMap[p.name.split("%%%")[1]] = p;
});
function groupSubscriptionsByProductName(subs) { // Step 3: Find children in API `data` and attach to parents
const result = {}; data
subs.forEach(sub => { .filter((p) => p.sub_product_of) // only those with a parent
const name = sub.product_name; .forEach((child) => {
const productId = sub.product_id; // ✅ Current logic — attach to the real parent
if (!result[name]) { const parent = productMap[child.sub_product_of];
result[name] = { if (parent) {
product_id: productId, parent.children.push(child);
product_name: name, }
unit_type: sub.unit_type, // New logic — attach to other products with the same sub_product_of
end_date: sub.end_date, Object.values(productMap).forEach((possibleParent) => {
quantity: 0, if (
subscriptions: [] possibleParent.id !== child.id && // not itself
}; possibleParent.sub_product_of === child.sub_product_of // same parent reference
} ) {
possibleParent.children.push(child);
const currentEnd = new Date(result[name].end_date); }
const thisEnd = new Date(sub.end_date);
if (thisEnd > currentEnd) {
result[name].end_date = sub.end_date;
}
if (sub.unit_type == 'token') {
result[name].quantity += sub.quantity ?? 0;
} else {
result[name].quantity += 1;
}
result[name].subscriptions.push(sub);
}); });
});
console.log(enrichedData)
setProducts(enrichedData);
})
.catch((err) => console.error("Fetch error:", err));
return result; }, [subscriptions]);
}
const groupedSubs = groupSubscriptionsByProductName(subscriptions); const handleSettingsChange = (field, value, nested = false) => {
const productIds = [...new Set(subscriptions.map(s => s.product_id))]; if (nested) {
setSettings((prev) => ({
...prev,
profile_data: { ...prev.profile_data, [field]: value }
}));
} else {
setSettings((prev) => ({ ...prev, [field]: value }));
}
};
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', { const saveSettings = () => {
method: 'POST', fetch(
headers: { "https://bot.kediritechnopark.com/webhook-test/user-production/data",
'Content-Type': 'application/json', {
}, method: "PUT",
body: JSON.stringify({ itemsId: productIds }), headers: {
}) "Content-Type": "application/json"
.then(res => res.json()) },
.then(data => { body: JSON.stringify(settings)
const enrichedData = Object.values(groupedSubs) }
.filter(group => data.some(p => p.id === group.product_id)) )
.map(group => { .then((res) => res.json())
const productData = data.find(p => p.id == group.product_id); .then(() => {
let image = productData?.image || ''; alert("Settings updated successfully!");
let description = productData?.description || ''; })
let site_url = productData?.site_url || ''; .catch((err) => alert("Error updating settings: " + err));
if (!image && productData?.sub_product_of) { };
const parent = data.find(p => p.id === productData.sub_product_of);
console.log(parent)
image = parent?.image || '';
description = parent?.description || '';
site_url = parent?.site_url || '';
}
console.log(site_url)
return {
id: group.product_id,
name: group.product_name,
type: productData?.type || 'product',
image: image,
description: description,
site_url: site_url,
price: productData?.price || 0,
currency: productData?.currency || 'IDR',
duration: productData?.duration || {},
sub_product_of: productData?.sub_product_of || null,
is_visible: productData?.is_visible ?? true,
unit_type: productData?.unit_type || group.unit_type,
quantity: group.quantity,
end_date: group.end_date,
children: [],
};
});
console.log(enrichedData)
setProducts(enrichedData);
})
.catch(err => console.error('Fetch error:', err));
}, [subscriptions]);
const features = [/* ... (tidak diubah) ... */]; return (
<div style={{ fontFamily: "Inter, system-ui, sans-serif" }}>
{/* Tabs Navigation */}
<div className={styles.navTabs}>
<button
className={`${styles.floatMenuItem} ${activeTab === "products" ? styles.floatMenuItemActive : ""
}`}
onClick={() => setActiveTab("products")}
>
<span className={styles.floatMenuTitle}>Produk Saya</span>
<Box size={16} className={styles.floatMenuIcon} />
</button>
return ( <button
<div style={{ fontFamily: 'Inter, system-ui, sans-serif' }}> className={`${styles.floatMenuItem} ${activeTab === "settings" ? styles.floatMenuItemActive : ""
{/* Courses Section */} }`}
<section className={styles.Section}> onClick={() => setActiveTab("settings")}
<div className={styles.coursesContainer}> >
<h2 className={styles.coursesTitle}>MY PRODUCTS</h2> <span className={styles.floatMenuTitle}>Profil Pengguna</span>
<div className={styles.coursesGrid}> <Settings size={16} className={styles.floatMenuIcon} />
{products && </button>
products[0]?.name &&
products.map(product => (
<div
key={product.name}
className={`${styles.courseCard} ${hoveredCard === product.name ? styles.courseCardHover : ''}`}
onClick={() => {
setSelectedProduct(product);
setShowedModal('product');
}}
onMouseEnter={() => setHoveredCard(product.name)}
onMouseLeave={() => setHoveredCard(null)}
>
<div>
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }} />
<div className={styles.courseContentTop}>
<h3 className={styles.courseTitle}>{product.name.split('%%%')[0]}</h3>
<p className={styles.courseDesc}>{product.description}</p>
</div>
</div>
<div className={styles.courseContentBottom}>
<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>
<button className="px-4 py-2 rounded-pill text-white" style={{ background: 'linear-gradient(to right, #6a59ff, #8261ee)', border: 'none' }} <button
onClick={() => { className={`${styles.floatMenuItem} ${activeTab === "orders" ? styles.floatMenuItemActive : ""
setSelectedProduct(product); }`}
setShowedModal('product'); onClick={() => setActiveTab("orders")}
}}>Perpanjang</button> >
</div> <span className={styles.floatMenuTitle}>Pembelian</span>
</div> <ShoppingCart size={16} className={styles.floatMenuIcon} />
))} </button>
</div>
{/* Tab Content */}
{activeTab === "products" && (
<section className={styles.Section}>
<div className={styles.coursesContainer}>
<div className={styles.coursesGrid}>
{products.map((product) => (
<div
key={product.name}
className={`${styles.courseCard} ${hoveredCard === product.name ? styles.courseCardHover : ""
}`}
onClick={() => {
setSelectedProduct(product);
setShowedModal("product");
}}
onMouseEnter={() => setHoveredCard(product.name)}
onMouseLeave={() => setHoveredCard(null)}
>
<div>
<div
className={styles.courseImage}
style={{
backgroundImage: `url(${product.image})`
}}
/>
<div className={styles.courseContentTop}>
<h3 className={styles.courseTitle}>
{product.name.split("%%%")[0]}
</h3>
<p className={styles.courseDesc}>
{product.description}
</p>
</div> </div>
</div>
<div className={styles.courseContentBottom}>
<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>
<button
className="px-4 py-2 rounded-pill text-white"
style={{
background:
"linear-gradient(to right, #6a59ff, #8261ee)",
border: "none"
}}
onClick={() => {
setSelectedProduct(product);
setShowedModal("product");
setWillDo('checkout');
}}
>
Perpanjang
</button>
</div>
</div> </div>
</section> ))}
</div > <div
); className={`${styles.courseCard} ${hoveredCard === 0 ? styles.courseCardHover : ""
}`}
onMouseEnter={() => setHoveredCard(0)}
onMouseLeave={() => setHoveredCard(null)}
onClick={() => navigate('/?tab=products')
}
>
<div>
+ Tambah produk baru</div>
</div>
</div>
</div>
</section>
)}
{activeTab === "settings" && (
<section className={styles.profileSection}>
<h2 className={styles.profileHeading}>Profil</h2>
<div className={styles.sectionDivider}></div>
<div className={styles.formGrid}>
<div>
<label className={styles.label}>Username</label>
<input
className={styles.inputField}
value={settings.username}
onChange={(e) =>
handleSettingsChange("username", e.target.value)
}
/>
</div>
<div>
<label className={styles.label}>Email</label>
<input
className={styles.inputField}
value={settings.email}
onChange={(e) =>
handleSettingsChange("email", e.target.value)
}
/>
</div>
<div>
<label className={styles.label}>Full Name</label>
<input
className={styles.inputField}
value={settings.profile_data.name}
onChange={(e) =>
handleSettingsChange("name", e.target.value, true)
}
/>
</div>
<div>
<label className={styles.label}>Phone</label>
<input
className={styles.inputField}
value={settings.profile_data.phone}
onChange={(e) =>
handleSettingsChange("phone", e.target.value, true)
}
/>
</div>
<div className={styles.fullWidth}>
<label className={styles.label}>Address</label>
<input
className={styles.inputField}
value={settings.profile_data.address}
onChange={(e) =>
handleSettingsChange("address", e.target.value, true)
}
/>
</div>
<div>
<label className={styles.label}>Company</label>
<input
className={styles.inputField}
value={settings.profile_data.company}
onChange={(e) =>
handleSettingsChange("company", e.target.value, true)
}
/>
</div>
<div>
<label className={styles.label}>Profile Image URL</label>
<input
className={styles.inputField}
value={settings.profile_data.image}
onChange={(e) =>
handleSettingsChange("image", e.target.value, true)
}
/>
</div>
</div>
<h2 className={styles.profileHeading}>Ganti password</h2>
<div className={styles.sectionDivider}></div>
<div className={styles.formGrid}>
<div>
<label className={styles.label}>New Password</label>
<input
className={styles.inputField}
type="password"
value={settings.password}
onChange={(e) =>
handleSettingsChange("password", e.target.value)
}
/>
</div>
<div>
<label className={styles.label}>Re-type New Password</label>
<input
className={styles.inputField}
type="password"
/>
</div>
</div>
<button className={styles.saveButton} onClick={saveSettings}>
Save Changes
</button>
</section>
)}
{activeTab === "orders" && (
<section className={styles.Section}>
<h2>My Orders</h2>
<p>Orders list will be displayed here.</p>
</section>
)}
</div>
);
}; };
export default CoursePage; export default Dashboard;