ok
This commit is contained in:
170
src/App.js
170
src/App.js
@@ -17,6 +17,7 @@ import Footer from './components/Footer';
|
||||
|
||||
import ProductDetailPage from './components/ProductDetailPage';
|
||||
|
||||
import Dashboard from './components/Dashboard';
|
||||
import ProductsPage from './components/pages/ProductsPage';
|
||||
|
||||
|
||||
@@ -43,7 +44,7 @@ function HomePage({
|
||||
setSelectedProduct={setSelectedProduct}
|
||||
setShowedModal={setShowedModal}
|
||||
/>
|
||||
<AcademySection
|
||||
<AcademySection
|
||||
courseSectionRef={courseSectionRef}
|
||||
hoveredCard={hoveredCard}
|
||||
setHoveredCard={setHoveredCard}
|
||||
@@ -56,19 +57,19 @@ function HomePage({
|
||||
);
|
||||
}
|
||||
function parseJwt(token) {
|
||||
try {
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join('')
|
||||
);
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join('')
|
||||
);
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
@@ -76,48 +77,51 @@ function App() {
|
||||
|
||||
// State yang diperlukan untuk HomePage
|
||||
const [hoveredCard, setHoveredCard] = useState(null);
|
||||
|
||||
const [subscriptions, setSubscriptions] = useState(null);
|
||||
const [selectedProduct, setSelectedProduct] = useState({});
|
||||
const [showedModal, setShowedModal] = useState(null); // 'product' | 'login' | null
|
||||
const [postLoginAction, setPostLoginAction] = useState(null);
|
||||
|
||||
|
||||
const [username, setUsername] = useState(null);
|
||||
|
||||
const productSectionRef = useRef(null);
|
||||
const courseSectionRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Ambil token dari cookies
|
||||
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];
|
||||
|
||||
fetch('https://bot.kediritechnopark.com/webhook/user-dev/data', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
fetch('https://bot.kediritechnopark.com/webhook/user-dev/data', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
|
||||
},
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
},
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
|
||||
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 (data && data.token) {
|
||||
// Update token with data[0].token
|
||||
document.cookie = `token=${data.token}; path=/`;
|
||||
console.log(data)
|
||||
setSubscriptions(data.subscriptions)
|
||||
const payload = parseJwt(data.token);
|
||||
if (payload && payload.username) {
|
||||
setUsername(payload.username);
|
||||
setUsername(payload.username);
|
||||
}
|
||||
} else {
|
||||
console.warn('Token tidak ditemukan dalam data.');
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Fetch error:', err));
|
||||
} else {
|
||||
console.warn('Token tidak ditemukan dalam data.');
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Fetch error:', err));
|
||||
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
}, []);
|
||||
const scrollToProduct = () => {
|
||||
productSectionRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
@@ -132,6 +136,17 @@ function App() {
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
// Hapus cookie token dengan mengatur tanggal kadaluarsa ke masa lalu
|
||||
document.cookie = 'token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
|
||||
|
||||
// Jika kamu menggunakan state seperti `setUsername`, bersihkan di sini juga
|
||||
setUsername(null); // jika applicable
|
||||
|
||||
// Redirect ke homepage atau reload halaman
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div id="js-preloader" className="js-preloader">
|
||||
@@ -150,7 +165,7 @@ function App() {
|
||||
return (
|
||||
<Router>
|
||||
<div className="App">
|
||||
<Header username={username} scrollToProduct={scrollToProduct} scrollToCourse={scrollToCourse} setShowedModal={setShowedModal} />
|
||||
<Header username={username} scrollToProduct={scrollToProduct} scrollToCourse={scrollToCourse} setShowedModal={setShowedModal} handleLogout={handleLogout} />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
@@ -170,41 +185,48 @@ function App() {
|
||||
<Route
|
||||
path="/products"
|
||||
element={
|
||||
<ProductsPage/>
|
||||
<ProductsPage subscriptions={subscriptions}/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<Dashboard />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<Footer />
|
||||
{/* Unified Modal */}
|
||||
{showedModal && (
|
||||
<div
|
||||
className={styles.modal}
|
||||
onClick={() => {
|
||||
setShowedModal(null);
|
||||
setSelectedProduct({});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.modalBody}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{showedModal === 'product' && (
|
||||
<ProductDetailPage
|
||||
setPostLoginAction={setPostLoginAction}
|
||||
setShowedModal={setShowedModal}
|
||||
product={selectedProduct}
|
||||
onClose={() => {
|
||||
setShowedModal(null);
|
||||
setSelectedProduct({});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showedModal === 'login' && (
|
||||
<Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Unified Modal */}
|
||||
{showedModal && (
|
||||
<div
|
||||
className={styles.modal}
|
||||
onClick={() => {
|
||||
setShowedModal(null);
|
||||
setSelectedProduct({});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.modalBody}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{showedModal === 'product' && (
|
||||
<ProductDetailPage
|
||||
subscriptions={subscriptions}
|
||||
setPostLoginAction={setPostLoginAction}
|
||||
setShowedModal={setShowedModal}
|
||||
product={selectedProduct}
|
||||
onClose={() => {
|
||||
setShowedModal(null);
|
||||
setSelectedProduct({});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showedModal === 'login' && (
|
||||
<Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
|
||||
280
src/components/Dashboard.js
Normal file
280
src/components/Dashboard.js
Normal file
@@ -0,0 +1,280 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { TrendingUp, TrendingDown, DollarSign, ShoppingCart, Users } from 'lucide-react';
|
||||
import styles from './Dashboard.module.css';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [dashboardData, setDashboardData] = useState({
|
||||
totalRevenue: {
|
||||
amount: 10215845,
|
||||
currency: 'IDR',
|
||||
change: 33.87,
|
||||
period: '22 - 29 May 2025'
|
||||
},
|
||||
totalItemsSold: {
|
||||
amount: 128980,
|
||||
change: -33.87,
|
||||
period: '22 - 29 May 2025'
|
||||
},
|
||||
totalVisitors: {
|
||||
amount: 2905897,
|
||||
change: 33.87,
|
||||
period: '22 - 29 May 2025'
|
||||
},
|
||||
chartData: [
|
||||
{ date: '22/06', items: 200, revenue: 800 },
|
||||
{ date: '23/06', items: 750, revenue: 450 },
|
||||
{ date: '24/06', items: 550, revenue: 200 },
|
||||
{ date: '24/06', items: 300, revenue: 350 },
|
||||
{ date: '24/06', items: 900, revenue: 450 },
|
||||
{ date: '24/06', items: 550, revenue: 200 },
|
||||
{ date: '24/06', items: 700, revenue: 300 }
|
||||
],
|
||||
latestTransactions: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Samantha William',
|
||||
amount: 250875,
|
||||
date: 'May 22, 2025',
|
||||
status: 'confirmed',
|
||||
avatar: 'SW'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Kevin Anderson',
|
||||
amount: 350620,
|
||||
date: 'May 22, 2025',
|
||||
status: 'waiting payment',
|
||||
avatar: 'KA'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Angela Samantha',
|
||||
amount: 870563,
|
||||
date: 'May 22, 2025',
|
||||
status: 'confirmed',
|
||||
avatar: 'AS'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Michael Smith',
|
||||
amount: 653975,
|
||||
date: 'May 22, 2025',
|
||||
status: 'payment expired',
|
||||
avatar: 'MS'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Jonathan Sebastian',
|
||||
amount: 950000,
|
||||
date: 'May 22, 2025',
|
||||
status: 'confirmed',
|
||||
avatar: 'JS'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Function untuk connect ke n8n webhook
|
||||
const connectToN8NWebhook = async (webhookUrl) => {
|
||||
try {
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setDashboardData(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error connecting to n8n webhook:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Function untuk send data ke n8n webhook
|
||||
const sendDataToN8N = async (webhookUrl, data) => {
|
||||
try {
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Data sent successfully to n8n');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending data to n8n:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('id-ID').format(amount);
|
||||
};
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
switch (status) {
|
||||
case 'confirmed':
|
||||
return styles.statusConfirmed;
|
||||
case 'waiting payment':
|
||||
return styles.statusWaiting;
|
||||
case 'payment expired':
|
||||
return styles.statusExpired;
|
||||
default:
|
||||
return styles.statusConfirmed;
|
||||
}
|
||||
};
|
||||
|
||||
const StatCard = ({ title, value, currency, change, period, icon: Icon, isNegative }) => (
|
||||
<div className={styles.statCard}>
|
||||
<div className={styles.statCardHeader}>
|
||||
<h3 className={styles.statCardTitle}>{title}</h3>
|
||||
<Icon className={styles.statCardIcon} />
|
||||
</div>
|
||||
|
||||
<div className={styles.statCardValue}>
|
||||
{currency && `${currency} `}{formatCurrency(value)}
|
||||
</div>
|
||||
|
||||
<div className={styles.statCardFooter}>
|
||||
<div className={styles.statCardChange}>
|
||||
{isNegative ? (
|
||||
<TrendingDown className={`${styles.trendIcon} ${styles.trendDown}`} />
|
||||
) : (
|
||||
<TrendingUp className={`${styles.trendIcon} ${styles.trendUp}`} />
|
||||
)}
|
||||
<span className={`${styles.changeText} ${isNegative ? styles.changeTextNegative : styles.changeTextPositive}`}>
|
||||
{Math.abs(change)}%
|
||||
</span>
|
||||
<span className={styles.fromLastWeek}>from last week</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.statCardPeriod}>{period}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const BarChart = ({ data }) => {
|
||||
const maxValue = Math.max(...data.map(item => Math.max(item.items, item.revenue)));
|
||||
|
||||
return (
|
||||
<div className={styles.barChart}>
|
||||
{data.map((item, index) => (
|
||||
<div key={index} className={styles.barGroup}>
|
||||
<div className={styles.barContainer}>
|
||||
<div
|
||||
className={`${styles.bar} ${styles.barItems}`}
|
||||
style={{
|
||||
height: `${(item.items / maxValue) * 200}px`
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.bar} ${styles.barRevenue}`}
|
||||
style={{
|
||||
height: `${(item.revenue / maxValue) * 200}px`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className={styles.barLabel}>{item.date}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{/* Stats Cards */}
|
||||
<div className={styles.statsGrid}>
|
||||
<StatCard
|
||||
title="Total Revenue"
|
||||
value={dashboardData.totalRevenue.amount}
|
||||
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>
|
||||
|
||||
{/* Charts and Transactions */}
|
||||
<div className={styles.chartsGrid}>
|
||||
{/* Report Statistics */}
|
||||
<div className={styles.chartCard}>
|
||||
<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.transactionsHeader}>
|
||||
<h3 className={styles.transactionsTitle}>Latest Transactions</h3>
|
||||
<a href="#" className={styles.seeAllLink}>see all transactions</a>
|
||||
</div>
|
||||
|
||||
<div className={styles.transactionsList}>
|
||||
{dashboardData.latestTransactions.map((transaction) => (
|
||||
<div key={transaction.id} className={styles.transactionItem}>
|
||||
<div className={styles.transactionLeft}>
|
||||
<div className={styles.transactionAvatar}>
|
||||
{transaction.avatar}
|
||||
</div>
|
||||
<div className={styles.transactionInfo}>
|
||||
<h4>{transaction.name}</h4>
|
||||
<p>on {transaction.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.transactionRight}>
|
||||
<span className={styles.transactionAmount}>
|
||||
IDR {formatCurrency(transaction.amount)}
|
||||
</span>
|
||||
<div className={`${styles.statusIndicator} ${getStatusClass(transaction.status)}`}></div>
|
||||
<span className={styles.transactionStatus}>
|
||||
{transaction.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
326
src/components/Dashboard.module.css
Normal file
326
src/components/Dashboard.module.css
Normal file
@@ -0,0 +1,326 @@
|
||||
/* dashboard.module.css */
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f9fafb;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.statCard:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.statCardHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.statCardTitle {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.statCardIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.statCardValue {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.statCardFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.statCardChange {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trendIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.trendUp {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.trendDown {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.changeText {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.changeTextPositive {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.changeTextNegative {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.fromLastWeek {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.statCardPeriod {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chartsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.chartsGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.chartCard {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.chartHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chartTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chartSubtitle {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
.chartLegend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.legendItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.legendColor {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.legendColorGreen {
|
||||
background-color: #10b981;
|
||||
}
|
||||
|
||||
.legendColorLightGreen {
|
||||
background-color: #6ee7b7;
|
||||
}
|
||||
|
||||
.legendText {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.barChart {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
height: 256px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.barGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.barContainer {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.barItems {
|
||||
background-color: #10b981;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.barRevenue {
|
||||
background-color: #6ee7b7;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.barLabel {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.transactionsHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.transactionsTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.seeAllLink {
|
||||
color: #3b82f6;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.seeAllLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.transactionsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.transactionItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.transactionItem:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.transactionLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.transactionAvatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #e5e7eb;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.transactionInfo h4 {
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.transactionInfo p {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.transactionRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.transactionAmount {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.statusIndicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.statusConfirmed {
|
||||
background-color: #10b981;
|
||||
}
|
||||
|
||||
.statusWaiting {
|
||||
background-color: #f59e0b;
|
||||
}
|
||||
|
||||
.statusExpired {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
.transactionStatus {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@@ -1,29 +1,23 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Navbar, Nav, Container } from 'react-bootstrap';
|
||||
import styles from './Styles.module.css';
|
||||
|
||||
const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal }) => {
|
||||
const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, handleLogout }) => {
|
||||
const navigate = useNavigate();
|
||||
const [hoveredNav, setHoveredNav] = useState(null);
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false); // toggle mobile menu
|
||||
|
||||
return (
|
||||
|
||||
<header className={styles.header}>
|
||||
<img src="./kediri-technopark-logo.png" className={styles.logo} alt="Logo" />
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className={styles.nav}>
|
||||
|
||||
<a
|
||||
className={`${styles.navLink} ${hoveredNav === 2 ? styles.navLinkHover : ''}`}
|
||||
onMouseEnter={() => setHoveredNav(2)}
|
||||
onMouseLeave={() => setHoveredNav(null)}
|
||||
onClick={() => {
|
||||
navigate('/');
|
||||
|
||||
}}
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
HOME
|
||||
</a>
|
||||
@@ -32,16 +26,11 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal }) =
|
||||
onMouseEnter={() => setHoveredNav(3)}
|
||||
onMouseLeave={() => setHoveredNav(null)}
|
||||
onClick={() => {
|
||||
if (username == null) {
|
||||
scrollToCourse();
|
||||
}
|
||||
else {
|
||||
navigate('/products');
|
||||
}
|
||||
if (!username) scrollToCourse();
|
||||
else navigate('/products');
|
||||
}}
|
||||
>
|
||||
{username == null ? "PRODUCTS" : "MY PRODUCTS"}
|
||||
|
||||
{username ? 'MY PRODUCTS' : 'PRODUCTS'}
|
||||
</a>
|
||||
<a
|
||||
className={`${styles.navLink} ${hoveredNav === 4 ? styles.navLinkHover : ''}`}
|
||||
@@ -52,12 +41,54 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal }) =
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
{/* Burger Menu Button */}
|
||||
<div className={styles.burger} onClick={() => setMenuOpen(!menuOpen)}>
|
||||
☰
|
||||
</div>
|
||||
|
||||
{/* Mobile Dropdown Menu */}
|
||||
{menuOpen && (
|
||||
<div className={styles.mobileMenu}>
|
||||
{username ? (
|
||||
<>
|
||||
<div className={styles.username}>Halo, {username}</div>
|
||||
|
||||
<button className={styles.logoutButton} onClick={() => {
|
||||
navigate('/products');
|
||||
}}>
|
||||
MY PRODUCTS
|
||||
</button>
|
||||
|
||||
<button className={styles.logoutButton} onClick={() => {
|
||||
setMenuOpen(false);
|
||||
handleLogout();
|
||||
}}>
|
||||
Logout
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className={styles.loginButton}
|
||||
onClick={() => {
|
||||
setMenuOpen(false);
|
||||
setShowedModal('login');
|
||||
}}
|
||||
>
|
||||
LOGIN
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desktop Auth Buttons */}
|
||||
<div className={styles.authButtons}>
|
||||
{username ? (
|
||||
<span style={{ color: '#2563eb', fontWeight: '600' }}>
|
||||
Halo, {username}
|
||||
</span>
|
||||
) : (
|
||||
{username && (
|
||||
<div className={styles.loggedInContainer}>
|
||||
<span className={styles.username}>Halo, {username}</span>
|
||||
<button className={styles.logoutButton} onClick={handleLogout}>Logout</button>
|
||||
</div>
|
||||
)}
|
||||
{!username && (
|
||||
<button className={styles.loginButton} onClick={() => setShowedModal('login')}>
|
||||
LOGIN
|
||||
</button>
|
||||
@@ -67,4 +98,4 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal }) =
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
export default Header;
|
||||
|
||||
@@ -1,241 +1,286 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import styles from './ProductDetail.module.css';
|
||||
|
||||
const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => {
|
||||
const [inCart, setInCart] = useState(false);
|
||||
const ProductDetail = ({ subscriptions, product, setPostLoginAction, setShowedModal }) => {
|
||||
const [showChildSelector, setShowChildSelector] = useState(false);
|
||||
const [selectedChildIds, setSelectedChildIds] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const existingCookie = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('itemsId='));
|
||||
let items = [];
|
||||
if (existingCookie) {
|
||||
try {
|
||||
const value = decodeURIComponent(existingCookie.split('=')[1]);
|
||||
items = JSON.parse(value);
|
||||
if (!Array.isArray(items)) items = [];
|
||||
} catch (e) {
|
||||
items = [];
|
||||
}
|
||||
const [matchingSubscriptions, setMatchingSubscriptions] = useState([]);
|
||||
const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(null);
|
||||
const [showSubscriptionSelector, setShowSubscriptionSelector] = useState(false);
|
||||
|
||||
const [showNamingInput, setShowNamingInput] = useState(false);
|
||||
const [customName, setCustomName] = useState('');
|
||||
|
||||
const parseJWT = (token) => {
|
||||
try {
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join('')
|
||||
);
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
setInCart(items.includes(product.id));
|
||||
}, [product.id]);
|
||||
|
||||
const onSetCart = () => {
|
||||
const existingCookie = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('itemsId='));
|
||||
|
||||
let items = [];
|
||||
|
||||
if (existingCookie) {
|
||||
try {
|
||||
const value = decodeURIComponent(existingCookie.split('=')[1]);
|
||||
items = JSON.parse(value);
|
||||
if (!Array.isArray(items)) items = [];
|
||||
} catch (e) {
|
||||
items = [];
|
||||
}
|
||||
}
|
||||
|
||||
let updatedItems;
|
||||
if (items.includes(product.id)) {
|
||||
updatedItems = items.filter(id => id !== product.id); // remove
|
||||
setInCart(false);
|
||||
} else {
|
||||
updatedItems = [...items, product.id]; // add
|
||||
setInCart(true);
|
||||
}
|
||||
|
||||
document.cookie = `itemsId=${JSON.stringify(updatedItems)}; path=/; max-age=${7 * 24 * 60 * 60}`;
|
||||
};
|
||||
|
||||
const onCheckout = () => {
|
||||
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] : '';
|
||||
|
||||
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('; ')
|
||||
.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 (!token) {
|
||||
setPostLoginAction(() => () => onCheckout());
|
||||
setShowedModal('login');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (product.type == 'product') {
|
||||
|
||||
// Tambahkan product.id jika belum ada
|
||||
if (!items.includes(product.id)) {
|
||||
items.push(product.id);
|
||||
}
|
||||
|
||||
const itemsParam = JSON.stringify(items);
|
||||
const hasMatchingSubscription = Array.isArray(subscriptions) &&
|
||||
subscriptions.some(sub =>
|
||||
sub.product_name?.toLowerCase().includes(product.name.toLowerCase())
|
||||
);
|
||||
|
||||
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] : '';
|
||||
// Always show children selector first if product has children
|
||||
if (product.children && product.children.length > 0) {
|
||||
setShowChildSelector(true);
|
||||
|
||||
if (selectedChildIds.length === 0) {
|
||||
alert('Pilih minimal satu produk');
|
||||
return;
|
||||
}
|
||||
if (hasMatchingSubscription) {
|
||||
const matching = subscriptions.filter(sub =>
|
||||
sub.product_name?.toLowerCase().includes(product.name.toLowerCase())
|
||||
);
|
||||
const uniqueByName = Array.from(new Map(matching.map(sub => [sub.product_name, sub])).values());
|
||||
|
||||
// Ambil itemsId dari cookie
|
||||
const itemsCookie = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('itemsId='));
|
||||
if (uniqueByName.length > 0) {
|
||||
setMatchingSubscriptions(uniqueByName);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No children, but has subscription match
|
||||
if (hasMatchingSubscription) {
|
||||
const matching = subscriptions.filter(sub =>
|
||||
sub.product_name?.toLowerCase().includes(product.name.toLowerCase())
|
||||
);
|
||||
const uniqueByName = Array.from(new Map(matching.map(sub => [sub.product_name, sub])).values());
|
||||
|
||||
if (uniqueByName.length > 0) {
|
||||
setMatchingSubscriptions(uniqueByName);
|
||||
setShowSubscriptionSelector(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let items = [];
|
||||
if (itemsCookie) {
|
||||
try {
|
||||
items = JSON.parse(itemsCookie.split('=')[1]);
|
||||
if (!Array.isArray(items)) items = [];
|
||||
} catch (e) {
|
||||
items = [];
|
||||
}
|
||||
}
|
||||
// No children, no matching subscription
|
||||
const itemsParam = JSON.stringify([product.id]);
|
||||
window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
||||
};
|
||||
|
||||
// Gabungkan items dari cookie dengan selectedChildIds
|
||||
const mergedItems = Array.from(new Set([...items, ...selectedChildIds]));
|
||||
const onConfirmChildren = () => {
|
||||
if (matchingSubscriptions.length > 0) {
|
||||
setShowChildSelector(false);
|
||||
setShowSubscriptionSelector(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsParam = JSON.stringify(mergedItems);
|
||||
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
|
||||
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
|
||||
|
||||
window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
||||
};
|
||||
if (selectedChildIds.length === 0) {
|
||||
alert('Pilih minimal satu produk');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsParam = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : JSON.stringify([product.id]);
|
||||
window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
||||
};
|
||||
|
||||
const onFinalCheckoutNewProduct = () => {
|
||||
if (!customName.trim()) {
|
||||
alert('Nama produk tidak boleh kosong');
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
|
||||
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
|
||||
const itemsParam = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : JSON.stringify([product.id]);
|
||||
const encodedName = encodeURIComponent(customName.trim());
|
||||
|
||||
window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
||||
};
|
||||
|
||||
const onConfirmSelector = () => {
|
||||
if (selectedSubscriptionId == null) {
|
||||
alert('Pilih salah satu langganan.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedSubscriptionId === product.id) {
|
||||
setShowNamingInput(true);
|
||||
} else {
|
||||
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
|
||||
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
|
||||
const itemsParam = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : JSON.stringify([product.id]);
|
||||
const selectedSubscription = matchingSubscriptions.find(
|
||||
(sub) => sub.id === selectedSubscriptionId
|
||||
);
|
||||
|
||||
const productName = selectedSubscription?.product_name;
|
||||
const encodedName = encodeURIComponent(productName);
|
||||
|
||||
window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&set_name=${encodedName}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
||||
}
|
||||
};
|
||||
|
||||
const priceColor = product.price === 0 ? '#059669' : '#2563eb';
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
|
||||
{/* ✅ Tampilan utama disembunyikan jika sedang memilih child */}
|
||||
{!showChildSelector && (
|
||||
{!showChildSelector && !showSubscriptionSelector && !showNamingInput && (
|
||||
<>
|
||||
<div
|
||||
className={styles.image}
|
||||
style={{ backgroundImage: `url(${product.image})` }}
|
||||
></div>
|
||||
|
||||
<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')}`}
|
||||
{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')}
|
||||
>
|
||||
<button className={`${styles.button} ${styles.checkoutButton}`} onClick={onCheckout}>
|
||||
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' }}>
|
||||
<label key={child.id} className={styles.childProduct}>
|
||||
<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));
|
||||
}
|
||||
setSelectedChildIds(prev =>
|
||||
checked ? [...prev, child.id] : prev.filter(id => id !== child.id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{' '}
|
||||
{child.name} — Rp {parseInt(child.price || 0).toLocaleString('id-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;
|
||||
})
|
||||
<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}>
|
||||
|
||||
<button
|
||||
className={`${styles.button} ${styles.cancelButton}`}
|
||||
onClick={() => {
|
||||
setShowChildSelector(false);
|
||||
setSelectedChildIds([]);
|
||||
}}
|
||||
>
|
||||
<button className={styles.button} onClick={() => setShowChildSelector(false)}>
|
||||
Kembali
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.button} ${styles.confirmButton}`}
|
||||
onClick={onConfirmChildren}
|
||||
>
|
||||
<button className={styles.button} onClick={onConfirmChildren}>
|
||||
Lanjut ke Checkout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSubscriptionSelector && !showNamingInput && (
|
||||
<div className={styles.childSelector}>
|
||||
<h5>Perpanjang {product.name}</h5>
|
||||
{matchingSubscriptions.map(sub => (
|
||||
<label key={sub.id} className={styles.childProduct}>
|
||||
<input
|
||||
type="radio"
|
||||
name="subscription"
|
||||
value={sub.id}
|
||||
checked={selectedSubscriptionId == sub.id}
|
||||
onChange={() => { setSelectedSubscriptionId(sub.id); setCustomName(sub.product_name) }}
|
||||
/>
|
||||
{sub.product_name}
|
||||
</label>
|
||||
))}
|
||||
<h6>Atau buat baru</h6>
|
||||
<label className={styles.childProduct}>
|
||||
<input
|
||||
type="radio"
|
||||
name="subscription"
|
||||
checked={selectedSubscriptionId === product.id}
|
||||
onChange={() => setSelectedSubscriptionId(product.id)}
|
||||
/>
|
||||
Buat {product.name} baru
|
||||
</label>
|
||||
<div className={styles.buttonGroup}>
|
||||
<button className={styles.button} onClick={() => setShowSubscriptionSelector(false)}>
|
||||
Kembali
|
||||
</button>
|
||||
<button className={styles.button} onClick={onConfirmSelector}>
|
||||
Lanjut ke Checkout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showNamingInput && (
|
||||
<div className={styles.childSelector}>
|
||||
<h5>Buat {product.name} Baru</h5>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nama produk..."
|
||||
className={styles.input}
|
||||
value={customName}
|
||||
onChange={(e) => setCustomName(e.target.value)}
|
||||
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}>
|
||||
<button
|
||||
className={styles.button}
|
||||
onClick={() => {
|
||||
setShowNamingInput(false);
|
||||
setShowSubscriptionSelector(true);
|
||||
}}
|
||||
>
|
||||
Kembali
|
||||
</button>
|
||||
<button
|
||||
className={styles.button}
|
||||
onClick={onFinalCheckoutNewProduct}
|
||||
disabled={
|
||||
customName.trim() === '' ||
|
||||
matchingSubscriptions.some(
|
||||
(sub) => sub.product_name === `${product.name}@${customName}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -542,9 +542,88 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.nav {
|
||||
display: none;
|
||||
}
|
||||
/* Burger icon (hanya muncul di mobile) */
|
||||
.burger {
|
||||
display: none;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobileMenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Tampilkan burger dan menu di mobile */
|
||||
@media (max-width: 600px) {
|
||||
.nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.burger {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.authButtons {
|
||||
display: none; /* sembunyikan tombol login/logout biasa */
|
||||
}
|
||||
|
||||
.mobileMenu {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.mobileMenu button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobileMenu button:hover {
|
||||
background-color: #1e40af;
|
||||
}
|
||||
|
||||
.mobileMenu .username {
|
||||
font-weight: bold;
|
||||
color: #2563eb;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.loggedInContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #2563eb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logoutButton {
|
||||
background-color: transparent;
|
||||
border: 1px solid #2563eb;
|
||||
color: #2563eb;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.logoutButton:hover {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -3,78 +3,60 @@ import ProductDetailPage from '../ProductDetailPage';
|
||||
import Login from '../Login';
|
||||
import styles from '../Styles.module.css';
|
||||
|
||||
|
||||
function parseJwt(token) {
|
||||
try {
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join('')
|
||||
);
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getDistinctProductIdsFromJwt(token) {
|
||||
const payload = parseJwt(token);
|
||||
if (!payload || !payload.subscriptions || !payload.subscriptions) return [];
|
||||
|
||||
const productIds = payload.subscriptions.map(p => p.product_id);
|
||||
return [...new Set(productIds)];
|
||||
}
|
||||
|
||||
function getLatestEndDatesFromJwt(token) {
|
||||
const payload = parseJwt(token);
|
||||
if (!payload || !payload.subscriptions || !payload.subscriptions) return {};
|
||||
|
||||
const result = {};
|
||||
payload.subscriptions.forEach(p => {
|
||||
if (!p.end_date) return;
|
||||
const id = p.product_id;
|
||||
const endDate = new Date(p.end_date);
|
||||
if (!result[id] || endDate > new Date(result[id])) {
|
||||
result[id] = p.end_date;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
function getTotalTokenFromJwt(token) {
|
||||
const payload = parseJwt(token);
|
||||
if (!payload || !payload.subscriptions || !payload.subscriptions) return {};
|
||||
|
||||
const tokenQuantities = {};
|
||||
payload.subscriptions.forEach(p => {
|
||||
// Pastikan ada quantity dan unit_type token
|
||||
if (p.quantity && p.product_id) {
|
||||
tokenQuantities[p.product_id] = (tokenQuantities[p.product_id] || 0) + p.quantity;
|
||||
}
|
||||
});
|
||||
|
||||
return tokenQuantities;
|
||||
}
|
||||
|
||||
|
||||
const CoursePage = () => {
|
||||
const CoursePage = ({ subscriptions }) => {
|
||||
const [postLoginAction, setPostLoginAction] = useState(null);
|
||||
const [selectedProduct, setSelectedProduct] = useState({});
|
||||
const [hoveredCard, setHoveredCard] = useState(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];
|
||||
|
||||
const productIds = getDistinctProductIdsFromJwt(token);
|
||||
const endDates = getLatestEndDatesFromJwt(token);
|
||||
const tokenQuantitiesFromJwt = getTotalTokenFromJwt(token);
|
||||
useEffect(() => {
|
||||
if (!subscriptions) return;
|
||||
|
||||
// Step 1: Group subscriptions by product_name
|
||||
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: []
|
||||
};
|
||||
}
|
||||
|
||||
// Update end_date jika lebih baru
|
||||
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;
|
||||
}
|
||||
|
||||
// Tambahkan quantity jika unit_type adalah 'token'
|
||||
if (sub.unit_type == 'token') {
|
||||
result[name].quantity += sub.quantity ?? 0;
|
||||
} else {
|
||||
result[name].quantity += 1; // Bisa diabaikan atau tetap hitung 1 per subscription
|
||||
}
|
||||
|
||||
result[name].subscriptions.push(sub);
|
||||
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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))];
|
||||
|
||||
// Step 3: Fetch product metadata
|
||||
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -84,57 +66,33 @@ useEffect(() => {
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const parentMap = {};
|
||||
const childrenMap = {};
|
||||
const enrichedData = Object.values(groupedSubs).map(group => {
|
||||
const productData = data.find(p => p.id == group.product_id);
|
||||
|
||||
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: []
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: group.product_id,
|
||||
name: group.product_name,
|
||||
type: productData?.type || 'product',
|
||||
image: productData?.image || '',
|
||||
description: productData?.description || '',
|
||||
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, // Bisa diganti dengan jumlah token kalau diperlukan
|
||||
end_date: group.end_date,
|
||||
children: [] // Kosong, bisa diisi jika ada sub-product
|
||||
};
|
||||
});
|
||||
// ...
|
||||
|
||||
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);
|
||||
console.log(enrichedData)
|
||||
setProducts(enrichedData);
|
||||
console.log(enrichedData);
|
||||
console.log('Enriched Data:', enrichedData);
|
||||
})
|
||||
.catch(err => console.error('Fetch error:', err));
|
||||
}
|
||||
}, []);
|
||||
}, [subscriptions]);
|
||||
|
||||
|
||||
|
||||
const features = [
|
||||
@@ -171,19 +129,19 @@ Object.values(parentMap).forEach(product => {
|
||||
products
|
||||
.map(product => (
|
||||
<div
|
||||
key={product.id}
|
||||
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
|
||||
key={product.name}
|
||||
className={`${styles.courseCard} ${hoveredCard === product.name ? styles.courseCardHover : ''}`}
|
||||
onClick={() => {
|
||||
setSelectedProduct(product);
|
||||
setShowedModal('product');
|
||||
}}
|
||||
onMouseEnter={() => setHoveredCard(product.id)}
|
||||
onMouseEnter={() => setHoveredCard(product.name)}
|
||||
onMouseLeave={() => setHoveredCard(null)}
|
||||
>
|
||||
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
|
||||
{product.price == 0 && (
|
||||
{/* {product.price == 0 && (
|
||||
<span className={styles.courseLabel}>Free</span>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
<div className={styles.courseContent}>
|
||||
<h3 className={styles.courseTitle}>{product.name}</h3>
|
||||
|
||||
Reference in New Issue
Block a user