This commit is contained in:
Vassshhh
2025-08-07 15:47:34 +07:00
parent f2b30f515c
commit 3cf86829ed
6 changed files with 499 additions and 357 deletions

View File

@@ -124,6 +124,7 @@ function App() {
.catch(err => console.error('Fetch error:', err)); .catch(err => console.error('Fetch error:', err));
} }
}, []); }, []);
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');
@@ -146,6 +147,7 @@ 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
} }
}, []); }, []);
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');
@@ -167,12 +169,15 @@ function App() {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!productModalRequest) return; console.log(subscriptions)
if (!productModalRequest || !subscriptions) return;
const { productId, authorizedUri, unauthorizedUri } = productModalRequest; const { productId, authorizedUri, unauthorizedUri } = productModalRequest;
console.log(subscriptions)
const hasAccess = subscriptions && subscriptions.some(sub => sub.product_id === productId); const hasAccess = subscriptions && subscriptions.some(
sub => sub.product_id === productId || sub.product_parent_id === productId
);
console.log(hasAccess)
if (hasAccess) { if (hasAccess) {
if (authorizedUri) { if (authorizedUri) {
let finalUri = decodeURIComponent(authorizedUri); let finalUri = decodeURIComponent(authorizedUri);

View File

@@ -1,8 +1,20 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { TrendingUp, TrendingDown, DollarSign, ShoppingCart, Users } from 'lucide-react'; import { TrendingUp, TrendingDown, DollarSign, ShoppingCart, Users } from 'lucide-react';
import styles from './Dashboard.module.css'; import styles from './Dashboard.module.css';
import processProducts from '../helper/processProducts';
const Dashboard = () => { const Dashboard = () => {
const [unitType, setUnitType] = useState('duration');
const [durationUnit, setDurationUnit] = useState('day');
const [availableTypes, setAvailableTypes] = useState([]);
const [availableGroups, setAvailableGroups] = useState([]);
const [selectedType, setSelectedType] = useState(null);
const [selectedGroup, setSelectedGroup] = useState(null);
const [isVisible, setIsVisible] = useState(true);
const [products, setProducts] = useState([]);
const [dashboardData, setDashboardData] = useState({ const [dashboardData, setDashboardData] = useState({
totalRevenue: { totalRevenue: {
amount: 10215845, amount: 10215845,
@@ -24,10 +36,9 @@ const Dashboard = () => {
{ date: '22/06', items: 200, revenue: 800 }, { date: '22/06', items: 200, revenue: 800 },
{ date: '23/06', items: 750, revenue: 450 }, { date: '23/06', items: 750, revenue: 450 },
{ date: '24/06', items: 550, revenue: 200 }, { date: '24/06', items: 550, revenue: 200 },
{ date: '24/06', items: 300, revenue: 350 }, { date: '25/06', items: 300, revenue: 350 },
{ date: '24/06', items: 900, revenue: 450 }, { date: '26/06', items: 900, revenue: 450 },
{ date: '24/06', items: 550, revenue: 200 }, { date: '27/06', items: 550, revenue: 200 },
{ date: '24/06', items: 700, revenue: 300 }
], ],
latestTransactions: [ latestTransactions: [
{ {
@@ -73,58 +84,88 @@ const Dashboard = () => {
] ]
}); });
// Function untuk connect ke n8n webhook useEffect(() => {
const connectToN8NWebhook = async (webhookUrl) => { const fetchDistinctOptions = async () => {
try { const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
const response = await fetch(webhookUrl, { if (!match) return;
method: 'GET', const token = match[2];
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) { try {
const data = await response.json(); const res = await fetch('https://bot.kediritechnopark.com/webhook/store-dev/get-products', {
setDashboardData(data); method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
const result = await res.json(); // hasil berupa array produk
const products = result || [];
// Ambil distinct `type` dan `group` manual
const types = [...new Set(products.map(p => p.type).filter(Boolean))];
const groups = [...new Set(products.map(p => p.group).filter(Boolean))];
setAvailableTypes(types);
setAvailableGroups(groups);
setProducts(processProducts(products));
} catch (err) {
console.error('Gagal ambil produk:', err);
} }
} catch (error) { };
console.error('Error connecting to n8n webhook:', error);
} fetchDistinctOptions();
}; }, []);
// Function untuk send data ke n8n webhook
const sendDataToN8N = async (webhookUrl, data) => { const sendDataToN8N = async (webhookUrl, data) => {
try { const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
const response = await fetch(webhookUrl, { if (match) {
method: 'POST', const token = match[2];
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) { const payload = {
console.log('Data sent successfully to n8n'); ...data,
duration: data.unit_type === 'token' ? null : data.duration,
quantity: data.unit_type === 'duration' ? null : data.quantity,
};
if (!token) {
alert('Token tidak ditemukan. Silakan login kembali.');
return;
}
try {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
if (response.ok) {
alert('Dorm berhasil ditambahkan!');
} else {
const errorText = await response.text();
console.error('Response Error:', errorText);
alert('Gagal mengirim data: ' + response.status);
}
} catch (error) {
console.error('Error sending data to n8n:', error);
alert('Terjadi kesalahan saat mengirim data.');
} }
} catch (error) {
console.error('Error sending data to n8n:', error);
} }
}; };
const formatCurrency = (amount) => { const formatCurrency = (amount) => new Intl.NumberFormat('id-ID').format(amount);
return new Intl.NumberFormat('id-ID').format(amount);
};
const getStatusClass = (status) => { const getStatusClass = (status) => {
switch (status) { switch (status) {
case 'confirmed': case 'confirmed': return styles.statusConfirmed;
return styles.statusConfirmed; case 'waiting payment': return styles.statusWaiting;
case 'waiting payment': case 'payment expired': return styles.statusExpired;
return styles.statusWaiting; default: return styles.statusConfirmed;
case 'payment expired':
return styles.statusExpired;
default:
return styles.statusConfirmed;
} }
}; };
@@ -134,11 +175,9 @@ const Dashboard = () => {
<h3 className={styles.statCardTitle}>{title}</h3> <h3 className={styles.statCardTitle}>{title}</h3>
<Icon className={styles.statCardIcon} /> <Icon className={styles.statCardIcon} />
</div> </div>
<div className={styles.statCardValue}> <div className={styles.statCardValue}>
{currency && `${currency} `}{formatCurrency(value)} {currency && `${currency} `}{formatCurrency(value)}
</div> </div>
<div className={styles.statCardFooter}> <div className={styles.statCardFooter}>
<div className={styles.statCardChange}> <div className={styles.statCardChange}>
{isNegative ? ( {isNegative ? (
@@ -152,31 +191,19 @@ const Dashboard = () => {
<span className={styles.fromLastWeek}>from last week</span> <span className={styles.fromLastWeek}>from last week</span>
</div> </div>
</div> </div>
<div className={styles.statCardPeriod}>{period}</div> <div className={styles.statCardPeriod}>{period}</div>
</div> </div>
); );
const BarChart = ({ data }) => { const BarChart = ({ data }) => {
const maxValue = Math.max(...data.map(item => Math.max(item.items, item.revenue))); const maxValue = Math.max(...data.map(item => Math.max(item.items, item.revenue)));
return ( return (
<div className={styles.barChart}> <div className={styles.barChart}>
{data.map((item, index) => ( {data.map((item, index) => (
<div key={index} className={styles.barGroup}> <div key={index} className={styles.barGroup}>
<div className={styles.barContainer}> <div className={styles.barContainer}>
<div <div className={`${styles.bar} ${styles.barItems}`} style={{ height: `${(item.items / maxValue) * 200}px` }} />
className={`${styles.bar} ${styles.barItems}`} <div className={`${styles.bar} ${styles.barRevenue}`} style={{ height: `${(item.revenue / maxValue) * 200}px` }} />
style={{
height: `${(item.items / maxValue) * 200}px`
}}
/>
<div
className={`${styles.bar} ${styles.barRevenue}`}
style={{
height: `${(item.revenue / maxValue) * 200}px`
}}
/>
</div> </div>
<span className={styles.barLabel}>{item.date}</span> <span className={styles.barLabel}>{item.date}</span>
</div> </div>
@@ -187,72 +214,26 @@ const Dashboard = () => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
{/* Stats Cards */}
<div className={styles.statsGrid}> <div className={styles.statsGrid}>
<StatCard <StatCard title="Total Revenue" value={dashboardData.totalRevenue.amount} currency="IDR" change={dashboardData.totalRevenue.change} period={dashboardData.totalRevenue.period} icon={DollarSign} isNegative={false} />
title="Total Revenue" <StatCard title="Total Items Sold" value={dashboardData.totalItemsSold.amount} change={dashboardData.totalItemsSold.change} period={dashboardData.totalItemsSold.period} icon={ShoppingCart} isNegative={true} />
value={dashboardData.totalRevenue.amount} <StatCard title="Total Visitor" value={dashboardData.totalVisitors.amount} change={dashboardData.totalVisitors.change} period={dashboardData.totalVisitors.period} icon={Users} isNegative={false} />
currency={dashboardData.totalRevenue.currency}
change={dashboardData.totalRevenue.change}
period={dashboardData.totalRevenue.period}
icon={DollarSign}
isNegative={false}
/>
<StatCard
title="Total Items Sold"
value={dashboardData.totalItemsSold.amount}
change={dashboardData.totalItemsSold.change}
period={dashboardData.totalItemsSold.period}
icon={ShoppingCart}
isNegative={true}
/>
<StatCard
title="Total Visitor"
value={dashboardData.totalVisitors.amount}
change={dashboardData.totalVisitors.change}
period={dashboardData.totalVisitors.period}
icon={Users}
isNegative={false}
/>
</div> </div>
{/* Charts and Transactions */}
<div className={styles.chartsGrid}> <div className={styles.chartsGrid}>
{/* Report Statistics */} {/* Chart and Transactions UI as before */}
<div className={styles.chartCard}> </div>
<div className={styles.chartHeader}>
<div>
<h3 className={styles.chartTitle}>Report Statistics</h3>
<p className={styles.chartSubtitle}>Period: 22 - 29 May 2025</p>
</div>
<div className={styles.chartLegend}>
<div className={styles.legendItem}>
<div className={`${styles.legendColor} ${styles.legendColorGreen}`}></div>
<span className={styles.legendText}>Items Sold</span>
</div>
<div className={styles.legendItem}>
<div className={`${styles.legendColor} ${styles.legendColorLightGreen}`}></div>
<span className={styles.legendText}>Revenue</span>
</div>
</div>
</div>
<BarChart data={dashboardData.chartData} />
</div>
{/* Latest Transactions */} {/* <div className={styles.chartCard}>
<div className={styles.chartCard}>
<div className={styles.transactionsHeader}> <div className={styles.transactionsHeader}>
<h3 className={styles.transactionsTitle}>Latest Transactions</h3> <h3 className={styles.transactionsTitle}>Latest Transactions</h3>
<a href="#" className={styles.seeAllLink}>see all transactions</a> <a href="#" className={styles.seeAllLink}>see all</a>
</div> </div>
<div className={styles.transactionsList}> <div className={styles.transactionsList}>
{dashboardData.latestTransactions.map((transaction) => ( {products.map((transaction) => (
<div key={transaction.id} className={styles.transactionItem}> <div key={transaction.id} className={styles.transactionItem}>
<div className={styles.transactionLeft}> <div className={styles.transactionLeft}>
<div className={styles.transactionAvatar}>
{transaction.avatar}
</div>
<div className={styles.transactionInfo}> <div className={styles.transactionInfo}>
<h4>{transaction.name}</h4> <h4>{transaction.name}</h4>
<p>on {transaction.date}</p> <p>on {transaction.date}</p>
@@ -271,7 +252,170 @@ const Dashboard = () => {
</div> </div>
))} ))}
</div> </div>
</div> */}
<div className={styles.chartCard}>
<div className={styles.transactionsHeader}>
<h3 className={styles.transactionsTitle}>Products</h3>
</div>
<div className={styles.transactionsList}>
{products.map((product) => (
<div key={product.id} className={styles.transactionItem}>
<div className={styles.transactionLeft}>
<div className={styles.transactionInfo}>
<h4>{product.name}</h4>
{product.children && product.children.map((child) => (
<p>- {child.name}</p>
))}
</div>
</div>
<div className={styles.transactionRight}>
<span className={styles.transactionAmount}>
IDR {formatCurrency(product.amount)}
</span>
<div className={`${styles.statusIndicator} ${getStatusClass(product.status)}`}></div>
<span className={styles.transactionStatus}>
{product.status}
</span>
</div>
</div>
))}
</div>
</div> </div>
<div className={styles.chartCard} style={{ marginTop: '2rem' }}>
<h3 className={styles.transactionsTitle}>Tambah Produk Baru</h3>
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target;
const isToken = unitType === 'token';
const durationValue = form.duration_value?.value;
const quantityValue = form.duration_quantity?.value;
const dormData = {
name: form.name.value,
type: selectedType,
image: form.image.value,
description: form.description.value,
price: parseInt(form.price.value, 10),
currency: 'IDR',
duration: isToken ? null : { [durationUnit]: parseInt(durationValue, 10) },
quantity: isToken ? parseInt(quantityValue, 10) : null,
unit_type: unitType,
sub_product_of: null,
is_visible: isVisible,
group: selectedGroup,
site_url: form.site_url.value || null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
sendDataToN8N('https://bot.kediritechnopark.com/webhook/store-dev/add-product', dormData);
}}
className={styles.form}
>
<div className={styles.formGroup}>
<label>Nama Produk</label>
<input type="text" name="name" required />
</div>
<div className={styles.formGroup}>
<label>Deskripsi</label>
<textarea name="description" rows={3} required />
</div>
<div className={styles.formGroup}>
<label>Harga</label>
<input type="number" name="price" required />
</div>
<div className={styles.formGroup}>
<label>Jenis Unit</label>
<select
name="unit_type"
value={unitType}
onChange={(e) => setUnitType(e.target.value)}
required
>
<option value="duration">Durasi</option>
<option value="token">Token</option>
</select>
</div>
{unitType === 'token' ? (
<div className={styles.formGroup}>
<label>Jumlah Token</label>
<input type="number" name="duration_quantity" required min="1" />
</div>
) : (
<div className={styles.formGroup}>
<label>Durasi</label>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<input type="number" name="duration_value" min="1" required />
<select name="duration_unit" value={durationUnit} onChange={(e) => setDurationUnit(e.target.value)} required>
<option value="day">Hari</option>
<option value="week">Minggu</option>
<option value="month">Bulan</option>
</select>
</div>
</div>
)}
<div className={styles.formGroup}>
<label>URL Gambar</label>
<input type="text" name="image" />
</div>
<div className={styles.formGroup}>
<label>Site URL (opsional)</label>
<input type="text" name="site_url" />
</div>
<div className={styles.formGroup}>
<label>Tipe Produk</label>
<input
type="text"
name="type"
value={selectedType || ''}
onChange={(e) => setSelectedType(e.target.value)}
required
/>
<div className={styles.suggestionContainer}>
{availableTypes.map((type) => (
<button
key={type}
type="button"
className={styles.suggestionButton}
onClick={() => setSelectedType(type)}
>
{type}
</button>
))}
</div>
</div>
<div className={styles.formGroup}>
<label>Group</label>
<input
type="text"
name="group"
value={selectedGroup || ''}
onChange={(e) => setSelectedGroup(e.target.value)}
/>
<div className={styles.suggestionContainer}>
{availableGroups.map((group) => (
<button
key={group}
type="button"
className={styles.suggestionButton}
onClick={() => setSelectedGroup(group)}
>
{group}
</button>
))}
</div>
</div>
<button type="submit" className={styles.submitButton}>Buat Produk</button>
</form>
</div> </div>
</div> </div>
); );

View File

@@ -225,7 +225,6 @@
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: #111827; color: #111827;
margin: 0;
} }
.seeAllLink { .seeAllLink {
@@ -324,3 +323,63 @@
color: #6b7280; color: #6b7280;
text-transform: capitalize; text-transform: capitalize;
} }
.form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.formGroup {
display: flex;
flex-direction: column;
}
.formGroup label {
font-weight: 500;
margin-bottom: 0.5rem;
}
.formGroup input,
.formGroup textarea {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 0.5rem;
}
.submitButton {
background-color: #2563eb;
color: white;
padding: 0.6rem 1rem;
border: none;
border-radius: 0.6rem;
cursor: pointer;
font-weight: 600;
}
.formGroup select {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 0.5rem;
}
.suggestionContainer {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.suggestionButton {
background-color: #eee;
border: none;
padding: 0.25rem 0.75rem;
border-radius: 12px;
cursor: pointer;
font-size: 0.85rem;
transition: background-color 0.2s;
}
.suggestionButton:hover {
background-color: #ccc;
}

View File

@@ -11,8 +11,7 @@ const LoginRegister = ({setShowedModal}) => {
backgroundColor: 'white', backgroundColor: 'white',
borderRadius: '1rem', borderRadius: '1rem',
padding: '2rem', padding: '2rem',
maxWidth: '400px', width: '100%',
margin: '0 auto',
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)', boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)',
fontFamily: 'Inter, system-ui, sans-serif', fontFamily: 'Inter, system-ui, sans-serif',
}, },

View File

@@ -38,47 +38,47 @@ const ProductDetail = ({ subscriptions, product, requestLogin, setShowedModal })
} }
if (product.type == 'product') { if (product.type == 'product') {
const hasMatchingSubscription = Array.isArray(subscriptions) && const hasMatchingSubscription = Array.isArray(subscriptions) &&
subscriptions.some(sub => subscriptions.some(sub =>
sub.product_id === product.id || sub.product_parent_id === 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 // Always show children selector first if product has children
if (product.children && product.children.length > 0) { if (product.children && product.children.length > 0) {
setShowChildSelector(true); setShowChildSelector(true);
if (hasMatchingSubscription) { if (hasMatchingSubscription) {
const matching = subscriptions.filter(sub => const matching = subscriptions.filter(sub =>
sub.product_id === product.id || sub.product_parent_id === product.id String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
); );
if (matching.length > 0) { if (matching.length > 0) {
// ✅ Select only the first for each product_name // ✅ Select only the first for each product_name
const uniqueByName = Array.from( const uniqueByName = Array.from(
new Map(matching.map(sub => [sub.product_name, sub])).values() new Map(matching.map(sub => [sub.product_name, sub])).values()
); );
setMatchingSubscriptions(uniqueByName); setMatchingSubscriptions(uniqueByName);
} }
} }
return; return;
} }
// No children, but has subscription match // No children, but has subscription match
if (hasMatchingSubscription) { if (hasMatchingSubscription) {
const matching = subscriptions.filter(sub => const matching = subscriptions.filter(sub =>
sub.product_id === product.id || sub.product_parent_id === product.id String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
); );
if (matching.length > 0) { if (matching.length > 0) {
const uniqueByName = Array.from( const uniqueByName = Array.from(
new Map(matching.map(sub => [sub.product_name, sub])).values() new Map(matching.map(sub => [sub.product_name, sub])).values()
); );
setMatchingSubscriptions(uniqueByName); setMatchingSubscriptions(uniqueByName);
setShowSubscriptionSelector(true); setShowSubscriptionSelector(true);
return; return;
} }
} }
} }
@@ -102,7 +102,7 @@ if (hasMatchingSubscription) {
return; return;
} }
const itemsParam = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : JSON.stringify([product.id]); const itemsParam = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : 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`;
}; };
@@ -114,7 +114,7 @@ if (hasMatchingSubscription) {
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 = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : 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`;
@@ -131,7 +131,7 @@ if (hasMatchingSubscription) {
} else { } else {
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 = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : JSON.stringify([product.id]);
const selectedSubscription = matchingSubscriptions.find( const selectedSubscription = matchingSubscriptions.find(
(sub) => sub.id === selectedSubscriptionId (sub) => sub.id === selectedSubscriptionId
); );
@@ -159,8 +159,12 @@ if (hasMatchingSubscription) {
<p className={styles.description}>{product.description}</p> <p className={styles.description}>{product.description}</p>
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<button className={`${styles.button} ${styles.checkoutButton}`} onClick={onCheckout}> <button className={`${styles.button} ${styles.checkoutButton}`} onClick={onCheckout}>
Checkout {Array.isArray(subscriptions) &&
subscriptions.some(sub =>
sub.product_id === product.id || sub.product_parent_id === product.id
) ? 'Perpanjang' : 'Checkout'}
</button> </button>
</div> </div>
</> </>
)} )}
@@ -237,53 +241,53 @@ if (hasMatchingSubscription) {
</div> </div>
)} )}
{showNamingInput && ( {showNamingInput && (
<div className={styles.childSelector}> <div className={styles.childSelector}>
<h5>Buat {product.name} Baru</h5> <h5>Buat {product.name} Baru</h5>
<input <input
type="text" type="text"
placeholder="Nama produk..." placeholder="Nama produk..."
className={styles.input} className={styles.input}
value={customName} value={customName}
onChange={(e) => setCustomName(e.target.value)} onChange={(e) => setCustomName(e.target.value)}
style={{ width: '100%', padding: '8px', marginBottom: '16px', borderRadius: '10px' }} style={{ width: '100%', padding: '8px', marginBottom: '16px', borderRadius: '10px' }}
/> />
{ {
matchingSubscriptions.some( matchingSubscriptions.some(
(sub) => sub.product_name === `${product.name}@${customName}` (sub) => sub.product_name === `${product.name}@${customName}`
) && ( ) && (
<p style={{ color: 'red', marginBottom: '10px' }}> <p style={{ color: 'red', marginBottom: '10px' }}>
Nama produk sudah digunakan. Nama produk sudah digunakan.
</p> </p>
) )
} }
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<button <button
className={styles.button} className={styles.button}
onClick={() => { onClick={() => {
setShowNamingInput(false); setShowNamingInput(false);
setShowSubscriptionSelector(true); 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( matchingSubscriptions.some(
(sub) => sub.product_name === `${product.name}@${customName}` (sub) => sub.product_name === `${product.name}@${customName}`
) )
} }
> >
Checkout Checkout
</button> </button>
</div> </div>
</div> </div>
)} )}
</div> </div>
); );

View File

@@ -10,10 +10,24 @@ const CoursePage = ({ subscriptions }) => {
const [showedModal, setShowedModal] = useState(null); const [showedModal, setShowedModal] = useState(null);
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
// Buka modal otomatis berdasarkan query
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const modal = urlParams.get('modal');
const productId = urlParams.get('product_id');
if (modal === 'product' && productId && products.length > 0) {
const product = products.find(p => String(p.id) === productId);
if (product) {
setSelectedProduct(product);
setShowedModal('product');
}
}
}, [products]);
useEffect(() => { useEffect(() => {
if (!subscriptions) return; if (!subscriptions) return;
// Step 1: Group subscriptions by product_name
function groupSubscriptionsByProductName(subs) { function groupSubscriptionsByProductName(subs) {
const result = {}; const result = {};
subs.forEach(sub => { subs.forEach(sub => {
@@ -30,33 +44,27 @@ const CoursePage = ({ subscriptions }) => {
}; };
} }
// Update end_date jika lebih baru
const currentEnd = new Date(result[name].end_date); const currentEnd = new Date(result[name].end_date);
const thisEnd = new Date(sub.end_date); const thisEnd = new Date(sub.end_date);
if (thisEnd > currentEnd) { if (thisEnd > currentEnd) {
result[name].end_date = sub.end_date; result[name].end_date = sub.end_date;
} }
// Tambahkan quantity jika unit_type adalah 'token'
if (sub.unit_type == 'token') { if (sub.unit_type == 'token') {
result[name].quantity += sub.quantity ?? 0; result[name].quantity += sub.quantity ?? 0;
} else { } else {
result[name].quantity += 1; // Bisa diabaikan atau tetap hitung 1 per subscription result[name].quantity += 1;
} }
result[name].subscriptions.push(sub); result[name].subscriptions.push(sub);
}); });
return result; return result;
} }
const groupedSubs = groupSubscriptionsByProductName(subscriptions); const groupedSubs = groupSubscriptionsByProductName(subscriptions);
// Step 2: Ambil semua unique product_id (tetap diperlukan untuk ambil metadata dari API)
const productIds = [...new Set(subscriptions.map(s => s.product_id))]; const productIds = [...new Set(subscriptions.map(s => s.product_id))];
// Step 3: Fetch product metadata
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', { fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -67,11 +75,9 @@ const CoursePage = ({ subscriptions }) => {
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
const enrichedData = Object.values(groupedSubs) const enrichedData = Object.values(groupedSubs)
.filter(group => data.some(p => p.id === group.product_id)) // ✅ hanya produk yang ada di metadata .filter(group => data.some(p => p.id === group.product_id))
.map(group => { .map(group => {
const productData = data.find(p => p.id == group.product_id); const productData = data.find(p => p.id == group.product_id);
// Cek fallback image dari parent jika image kosong dan sub_product_of ada
let image = productData?.image || ''; let image = productData?.image || '';
let description = productData?.description || ''; let description = productData?.description || '';
if (!image && productData?.sub_product_of) { if (!image && productData?.sub_product_of) {
@@ -94,43 +100,20 @@ const CoursePage = ({ subscriptions }) => {
unit_type: productData?.unit_type || group.unit_type, unit_type: productData?.unit_type || group.unit_type,
quantity: group.quantity, quantity: group.quantity,
end_date: group.end_date, end_date: group.end_date,
children: [] children: [],
site_url: productData?.site_url || ''
}; };
}); });
console.log(enrichedData)
setProducts(enrichedData); setProducts(enrichedData);
console.log('Enriched Data:', enrichedData);
}) })
.catch(err => console.error('Fetch error:', err)); .catch(err => console.error('Fetch error:', err));
}, [subscriptions]); }, [subscriptions]);
const features = [/* ... (tidak diubah) ... */];
const features = [
{
icon: '🌐',
title: 'Belajar Langsung dari Mentor Terbaik',
description:
'Kursus kami dirancang dan dipandu oleh para praktisi, pengajar, dan mentor yang ahli di bidangnya—mulai dari bisnis digital, teknologi, desain, hingga kecerdasan buatan. Semua materi disemakan dengan bahasa yang sederhana, mudah dipahami, dan langsung bisa dipraktikkan.',
},
{
icon: '⏰',
title: 'Fleksibel Sesuai Gaya Hidupmu',
description:
'Sibuk kerja? Urus anak? Atau lagi nyantai belajar Teknilog, di Akademi ini kamu bisa belajar kapan saja di mana saja, tanpa terikat waktu. Semua kursus kami bisa diakses ulang dan kamu bebas atur ritme belajar mu sendiri. Bebas lekukan, makamali ngatif.',
},
{
icon: '⚡',
title: 'Belajar Cepat, Dampak Nyata',
description:
'Kami percaya proses belajar tidak harus lama lama! Dengan pendekatan yang tepat, kamu bisa menguasai keterampilan baru hanya dalam hitungan minggu—buken bulan! Mulai dari belajar desain, digital marketing, AI, hingga manajemen usaha, semua bisa kamu kuasai dengan cepat dan tepat guna.',
},
];
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}>
@@ -138,115 +121,50 @@ const CoursePage = ({ subscriptions }) => {
<div className={styles.coursesGrid}> <div className={styles.coursesGrid}>
{products && {products &&
products[0]?.name && products[0]?.name &&
products products.map(product => (
.map(product => ( <div
<div key={product.name}
key={product.name} className={`${styles.courseCard} ${hoveredCard === product.name ? styles.courseCardHover : ''}`}
className={`${styles.courseCard} ${hoveredCard === product.name ? styles.courseCardHover : ''}`} onClick={() => {
onClick={() => { setSelectedProduct(product);
setSelectedProduct(product); setShowedModal('product');
setShowedModal('product'); }}
}} onMouseEnter={() => setHoveredCard(product.name)}
onMouseEnter={() => setHoveredCard(product.name)} onMouseLeave={() => setHoveredCard(null)}
onMouseLeave={() => setHoveredCard(null)} >
> <div>
<div> <div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }} />
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
{/* {product.price == 0 && (
<span className={styles.courseLabel}>Free</span>
)} */}
</div>
<div className={styles.courseContentTop}> <div className={styles.courseContentTop}>
<h3 className={styles.courseTitle}>{product.name}</h3> <h3 className={styles.courseTitle}>{product.name}</h3>
<p className={styles.courseDesc}>{product.description}</p> <p className={styles.courseDesc}>{product.description}</p>
</div> </div>
</div> </div>
<div className={styles.courseContentBottom}> <div className={styles.courseContentBottom}>
<div className={styles.coursePrice}> <div className={styles.coursePrice}>
<span <span
className={ className={
product.price == 0 product.price == 0
? styles.freePrice ? styles.freePrice
: styles.currentPrice : styles.currentPrice
} }
> >
{product.unit_type === 'duration' {product.unit_type === 'duration'
? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : 'N/A'}` ? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : 'N/A'}`
: `SISA TOKEN ${product.quantity || 0}` : `SISA TOKEN ${product.quantity || 0}`}
} </span>
</span>
</div>
</div> </div>
</div> </div>
))} </div>
))}
</div> </div>
</div> </div>
</section> </section>
{/* Features Section */} {/* Features Section */}
<section className={styles.Section}> {/* ... tidak berubah ... */}
<div className={styles.featuresContainer}>
<h2 className={styles.featuresTitle}>Mengapa Memilih Akademi Kami?</h2>
<p className={styles.featuresDescription}>
Di era digital yang terus berubah, Akademi kami hadir sebagai ruang tumbuh untuk siapa saja yang ingin berkembang.
Baik pelajar, profesional, UMKM, hingga pemula teknologikami bantu kamu naik level dengan materi praktis,
akses mudah, dan komunitas suportif.
</p>
<div className={styles.featuresList}>
{features.map((feature, index) => (
<div key={index} className={styles.featureItem}>
<div className={styles.featureIcon}>{feature.icon}</div>
<div className={styles.featureContent}>
<h3 className={styles.featureTitle}>{feature.title}</h3>
<p className={styles.featureDescription}>{feature.description}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className={styles.Section}>
<div className={styles.ctaContainer}>
<div className={styles.ctaCard}>
<div>
<div className={styles.ctaIcon}>😊</div>
<h3 className={styles.ctaTitle}>Murid Daftar Disini</h3>
<p className={styles.ctaDescription}>
Ambil langkah pertama menuju karier impian atau hobi barumu bersama Akademi Kami.
Belajar dengan cara yang menyenangkan, fleksibel, dan penuh manfaat.
</p>
</div>
<button className={styles.ctaButton}>START LEARNING</button>
</div>
<div className={styles.ctaCard}>
<div>
<div className={styles.ctaIcon}>👨🏫</div>
<h3 className={styles.ctaTitle}>Guru Daftar Disini</h3>
<p className={styles.ctaDescription}>
Ajarkan apa yang kamu cintai. Akademi kami memberikan semua alat
dan dukungan yang kamu butuhkan untuk membuat kursusmu sendiri.
</p>
</div>
<button className={styles.ctaButton}>START TEACHING</button>
</div>
</div>
</section>
{/* Footer */} {/* Footer */}
<footer className={styles.footer}> {/* ... tidak berubah ... */}
<div className={styles.footerContent}>
<p className={styles.footerText}>Created by Academy Kediri Techno Park</p>
<div className={styles.socialLinks}>
<a href="#" className={styles.socialLink}>📷</a>
<a href="#" className={styles.socialLink}>📱</a>
<a href="#" className={styles.socialLink}>📧</a>
</div>
</div>
</footer>
{/* Unified Modal */} {/* Unified Modal */}
{showedModal && ( {showedModal && (
@@ -257,20 +175,33 @@ const CoursePage = ({ subscriptions }) => {
setSelectedProduct({}); setSelectedProduct({});
}} }}
> >
<div <div className={styles.modalBody} onClick={(e) => e.stopPropagation()}>
className={styles.modalBody}
onClick={(e) => e.stopPropagation()}
>
{showedModal === 'product' && ( {showedModal === 'product' && (
<ProductDetailPage <div>
setPostLoginAction={setPostLoginAction} <ProductDetailPage
setShowedModal={setShowedModal} setPostLoginAction={setPostLoginAction}
product={selectedProduct} setShowedModal={setShowedModal}
onClose={() => { product={selectedProduct}
setShowedModal(null); subscriptions={subscriptions}
setSelectedProduct({}); onClose={() => {
}} setShowedModal(null);
/> setSelectedProduct({});
}}
/>
{/* Tombol KUNJUNGI */}
{selectedProduct.site_url && (
<a
href={`${selectedProduct.site_url}?token=${localStorage.getItem("token")}`}
target="_blank"
rel="noopener noreferrer"
className={styles.ctaButton}
style={{ marginTop: '1rem' }}
>
KUNJUNGI
</a>
)}
</div>
)} )}
{showedModal === 'login' && ( {showedModal === 'login' && (
<Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} /> <Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} />