ok
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
|
"lucide-react": "^0.536.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
@@ -11408,6 +11409,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.536.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.536.0.tgz",
|
||||||
|
"integrity": "sha512-2PgvNa9v+qz4Jt/ni8vPLt4jwoFybXHuubQT8fv4iCW5TjDxkbZjNZZHa485ad73NSEn/jdsEtU57eE1g+ma8A==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lz-string": {
|
"node_modules/lz-string": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
|
"lucide-react": "^0.536.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|||||||
34
src/App.js
34
src/App.js
@@ -17,6 +17,7 @@ import Footer from './components/Footer';
|
|||||||
|
|
||||||
import ProductDetailPage from './components/ProductDetailPage';
|
import ProductDetailPage from './components/ProductDetailPage';
|
||||||
|
|
||||||
|
import Dashboard from './components/Dashboard';
|
||||||
import ProductsPage from './components/pages/ProductsPage';
|
import ProductsPage from './components/pages/ProductsPage';
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +77,8 @@ function App() {
|
|||||||
|
|
||||||
// State yang diperlukan untuk HomePage
|
// State yang diperlukan untuk HomePage
|
||||||
const [hoveredCard, setHoveredCard] = useState(null);
|
const [hoveredCard, setHoveredCard] = useState(null);
|
||||||
|
|
||||||
|
const [subscriptions, setSubscriptions] = useState(null);
|
||||||
const [selectedProduct, setSelectedProduct] = useState({});
|
const [selectedProduct, setSelectedProduct] = useState({});
|
||||||
const [showedModal, setShowedModal] = useState(null); // 'product' | 'login' | null
|
const [showedModal, setShowedModal] = useState(null); // 'product' | 'login' | null
|
||||||
const [postLoginAction, setPostLoginAction] = useState(null);
|
const [postLoginAction, setPostLoginAction] = useState(null);
|
||||||
@@ -102,11 +105,12 @@ function App() {
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|
||||||
if (data && data[0] && data[0].token) {
|
if (data && data.token) {
|
||||||
// Update token with data[0].token
|
// Update token with data[0].token
|
||||||
document.cookie = `token=${data[0].token}; path=/`;
|
document.cookie = `token=${data.token}; path=/`;
|
||||||
|
console.log(data)
|
||||||
const payload = parseJwt(token);
|
setSubscriptions(data.subscriptions)
|
||||||
|
const payload = parseJwt(data.token);
|
||||||
if (payload && payload.username) {
|
if (payload && payload.username) {
|
||||||
setUsername(payload.username);
|
setUsername(payload.username);
|
||||||
}
|
}
|
||||||
@@ -132,6 +136,17 @@ function App() {
|
|||||||
return () => clearTimeout(timer);
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div id="js-preloader" className="js-preloader">
|
<div id="js-preloader" className="js-preloader">
|
||||||
@@ -150,7 +165,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Header username={username} scrollToProduct={scrollToProduct} scrollToCourse={scrollToCourse} setShowedModal={setShowedModal} />
|
<Header username={username} scrollToProduct={scrollToProduct} scrollToCourse={scrollToCourse} setShowedModal={setShowedModal} handleLogout={handleLogout} />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
@@ -170,7 +185,13 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="/products"
|
path="/products"
|
||||||
element={
|
element={
|
||||||
<ProductsPage/>
|
<ProductsPage subscriptions={subscriptions}/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/dashboard"
|
||||||
|
element={
|
||||||
|
<Dashboard />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
@@ -190,6 +211,7 @@ function App() {
|
|||||||
>
|
>
|
||||||
{showedModal === 'product' && (
|
{showedModal === 'product' && (
|
||||||
<ProductDetailPage
|
<ProductDetailPage
|
||||||
|
subscriptions={subscriptions}
|
||||||
setPostLoginAction={setPostLoginAction}
|
setPostLoginAction={setPostLoginAction}
|
||||||
setShowedModal={setShowedModal}
|
setShowedModal={setShowedModal}
|
||||||
product={selectedProduct}
|
product={selectedProduct}
|
||||||
|
|||||||
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 { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { Navbar, Nav, Container } from 'react-bootstrap';
|
|
||||||
import styles from './Styles.module.css';
|
import styles from './Styles.module.css';
|
||||||
|
|
||||||
const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal }) => {
|
const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, handleLogout }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [hoveredNav, setHoveredNav] = useState(null);
|
const [hoveredNav, setHoveredNav] = useState(null);
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false); // toggle mobile menu
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<img src="./kediri-technopark-logo.png" className={styles.logo} alt="Logo" />
|
<img src="./kediri-technopark-logo.png" className={styles.logo} alt="Logo" />
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
className={`${styles.navLink} ${hoveredNav === 2 ? styles.navLinkHover : ''}`}
|
className={`${styles.navLink} ${hoveredNav === 2 ? styles.navLinkHover : ''}`}
|
||||||
onMouseEnter={() => setHoveredNav(2)}
|
onMouseEnter={() => setHoveredNav(2)}
|
||||||
onMouseLeave={() => setHoveredNav(null)}
|
onMouseLeave={() => setHoveredNav(null)}
|
||||||
onClick={() => {
|
onClick={() => navigate('/')}
|
||||||
navigate('/');
|
|
||||||
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
HOME
|
HOME
|
||||||
</a>
|
</a>
|
||||||
@@ -32,16 +26,11 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal }) =
|
|||||||
onMouseEnter={() => setHoveredNav(3)}
|
onMouseEnter={() => setHoveredNav(3)}
|
||||||
onMouseLeave={() => setHoveredNav(null)}
|
onMouseLeave={() => setHoveredNav(null)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (username == null) {
|
if (!username) scrollToCourse();
|
||||||
scrollToCourse();
|
else navigate('/products');
|
||||||
}
|
|
||||||
else {
|
|
||||||
navigate('/products');
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{username == null ? "PRODUCTS" : "MY PRODUCTS"}
|
{username ? 'MY PRODUCTS' : 'PRODUCTS'}
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className={`${styles.navLink} ${hoveredNav === 4 ? styles.navLinkHover : ''}`}
|
className={`${styles.navLink} ${hoveredNav === 4 ? styles.navLinkHover : ''}`}
|
||||||
@@ -52,12 +41,54 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal }) =
|
|||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className={styles.authButtons}>
|
{/* Burger Menu Button */}
|
||||||
|
<div className={styles.burger} onClick={() => setMenuOpen(!menuOpen)}>
|
||||||
|
☰
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Dropdown Menu */}
|
||||||
|
{menuOpen && (
|
||||||
|
<div className={styles.mobileMenu}>
|
||||||
{username ? (
|
{username ? (
|
||||||
<span style={{ color: '#2563eb', fontWeight: '600' }}>
|
<>
|
||||||
Halo, {username}
|
<div className={styles.username}>Halo, {username}</div>
|
||||||
</span>
|
|
||||||
|
<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 && (
|
||||||
|
<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')}>
|
<button className={styles.loginButton} onClick={() => setShowedModal('login')}>
|
||||||
LOGIN
|
LOGIN
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,104 +1,96 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import styles from './ProductDetail.module.css';
|
import styles from './ProductDetail.module.css';
|
||||||
|
|
||||||
const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => {
|
const ProductDetail = ({ subscriptions, product, setPostLoginAction, setShowedModal }) => {
|
||||||
const [inCart, setInCart] = useState(false);
|
|
||||||
const [showChildSelector, setShowChildSelector] = useState(false);
|
const [showChildSelector, setShowChildSelector] = useState(false);
|
||||||
const [selectedChildIds, setSelectedChildIds] = useState([]);
|
const [selectedChildIds, setSelectedChildIds] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
const [matchingSubscriptions, setMatchingSubscriptions] = useState([]);
|
||||||
const existingCookie = document.cookie
|
const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(null);
|
||||||
.split('; ')
|
const [showSubscriptionSelector, setShowSubscriptionSelector] = useState(false);
|
||||||
.find(row => row.startsWith('itemsId='));
|
|
||||||
let items = [];
|
const [showNamingInput, setShowNamingInput] = useState(false);
|
||||||
if (existingCookie) {
|
const [customName, setCustomName] = useState('');
|
||||||
|
|
||||||
|
const parseJWT = (token) => {
|
||||||
try {
|
try {
|
||||||
const value = decodeURIComponent(existingCookie.split('=')[1]);
|
const base64Url = token.split('.')[1];
|
||||||
items = JSON.parse(value);
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
if (!Array.isArray(items)) items = [];
|
const jsonPayload = decodeURIComponent(
|
||||||
} catch (e) {
|
atob(base64)
|
||||||
items = [];
|
.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 onCheckout = () => {
|
||||||
const tokenCookie = document.cookie
|
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
|
||||||
.split('; ')
|
|
||||||
.find(row => row.startsWith('token='));
|
|
||||||
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
|
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
|
||||||
|
|
||||||
if (!tokenCookie) {
|
if (!token) {
|
||||||
setPostLoginAction(() => () => onCheckout());
|
setPostLoginAction(() => () => onCheckout());
|
||||||
setShowedModal('login');
|
setShowedModal('login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (product.type == 'product') {
|
||||||
|
|
||||||
// Jika punya children, tampilkan pilihan
|
|
||||||
|
const hasMatchingSubscription = Array.isArray(subscriptions) &&
|
||||||
|
subscriptions.some(sub =>
|
||||||
|
sub.product_name?.toLowerCase().includes(product.name.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ambil itemsId dari cookie
|
// No children, but has subscription match
|
||||||
const itemsCookie = document.cookie
|
if (hasMatchingSubscription) {
|
||||||
.split('; ')
|
const matching = subscriptions.filter(sub =>
|
||||||
.find(row => row.startsWith('itemsId='));
|
sub.product_name?.toLowerCase().includes(product.name.toLowerCase())
|
||||||
|
);
|
||||||
|
const uniqueByName = Array.from(new Map(matching.map(sub => [sub.product_name, sub])).values());
|
||||||
|
|
||||||
let items = [];
|
if (uniqueByName.length > 0) {
|
||||||
if (itemsCookie) {
|
setMatchingSubscriptions(uniqueByName);
|
||||||
try {
|
setShowSubscriptionSelector(true);
|
||||||
items = JSON.parse(itemsCookie.split('=')[1]);
|
return;
|
||||||
if (!Array.isArray(items)) items = [];
|
|
||||||
} catch (e) {
|
|
||||||
items = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tambahkan product.id jika belum ada
|
|
||||||
if (!items.includes(product.id)) {
|
|
||||||
items.push(product.id);
|
|
||||||
}
|
}
|
||||||
|
// No children, no matching subscription
|
||||||
const itemsParam = JSON.stringify(items);
|
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`;
|
window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConfirmChildren = () => {
|
const onConfirmChildren = () => {
|
||||||
const tokenCookie = document.cookie
|
if (matchingSubscriptions.length > 0) {
|
||||||
.split('; ')
|
setShowChildSelector(false);
|
||||||
.find(row => row.startsWith('token='));
|
setShowSubscriptionSelector(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
if (selectedChildIds.length === 0) {
|
||||||
@@ -106,136 +98,189 @@ const onConfirmChildren = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ambil itemsId dari cookie
|
const itemsParam = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : JSON.stringify([product.id]);
|
||||||
const itemsCookie = document.cookie
|
|
||||||
.split('; ')
|
|
||||||
.find(row => row.startsWith('itemsId='));
|
|
||||||
|
|
||||||
let items = [];
|
|
||||||
if (itemsCookie) {
|
|
||||||
try {
|
|
||||||
items = JSON.parse(itemsCookie.split('=')[1]);
|
|
||||||
if (!Array.isArray(items)) items = [];
|
|
||||||
} catch (e) {
|
|
||||||
items = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gabungkan items dari cookie dengan selectedChildIds
|
|
||||||
const mergedItems = Array.from(new Set([...items, ...selectedChildIds]));
|
|
||||||
|
|
||||||
const itemsParam = JSON.stringify(mergedItems);
|
|
||||||
|
|
||||||
window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
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';
|
const priceColor = product.price === 0 ? '#059669' : '#2563eb';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
{!showChildSelector && !showSubscriptionSelector && !showNamingInput && (
|
||||||
{/* ✅ Tampilan utama disembunyikan jika sedang memilih child */}
|
|
||||||
{!showChildSelector && (
|
|
||||||
<>
|
<>
|
||||||
<div
|
<div className={styles.image} style={{ backgroundImage: `url(${product.image})` }}></div>
|
||||||
className={styles.image}
|
|
||||||
style={{ backgroundImage: `url(${product.image})` }}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div className={styles.headerRow}>
|
<div className={styles.headerRow}>
|
||||||
<h2 className={styles.title}>{product.name}</h2>
|
<h2 className={styles.title}>{product.name}</h2>
|
||||||
<div className={styles.price} style={{ color: priceColor }}>
|
<div className={styles.price} style={{ color: priceColor }}>
|
||||||
{product.price == null
|
{product.price == null ? 'Pay-As-You-Go' : `Rp ${parseInt(product.price).toLocaleString('id-ID')}`}
|
||||||
? 'Pay-As-You-Go'
|
|
||||||
: `Rp ${parseInt(product.price).toLocaleString('id-ID')}`}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className={styles.description}>{product.description}</p>
|
<p className={styles.description}>{product.description}</p>
|
||||||
|
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
<button
|
<button className={`${styles.button} ${styles.checkoutButton}`} onClick={onCheckout}>
|
||||||
className={`${styles.button} ${styles.addToCartButton}`}
|
|
||||||
onClick={onSetCart}
|
|
||||||
onMouseOver={e => (e.target.style.backgroundColor = '#facc15')}
|
|
||||||
onMouseOut={e => (e.target.style.backgroundColor = '#fbbf24')}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={'/cart-shopping-svgrepo-com.svg'}
|
|
||||||
alt={inCart ? 'Hapus' : 'Tambah'}
|
|
||||||
style={{ width: '21px', height: '21px', marginRight: '7px' }}
|
|
||||||
/>
|
|
||||||
{inCart ? 'Hapus' : 'Tambah'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`${styles.button} ${styles.checkoutButton}`}
|
|
||||||
onClick={onCheckout}
|
|
||||||
onMouseOver={e => (e.target.style.backgroundColor = '#1d4ed8')}
|
|
||||||
onMouseOut={e => (e.target.style.backgroundColor = '#2563eb')}
|
|
||||||
>
|
|
||||||
Checkout
|
Checkout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ✅ UI pemilihan child */}
|
|
||||||
{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} style={{ display: 'block', marginBottom: '8px' }}>
|
<label key={child.id} className={styles.childProduct}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
value={child.id}
|
value={child.id}
|
||||||
checked={selectedChildIds.includes(child.id)}
|
checked={selectedChildIds.includes(child.id)}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
const checked = e.target.checked;
|
const checked = e.target.checked;
|
||||||
if (checked) {
|
setSelectedChildIds(prev =>
|
||||||
setSelectedChildIds(prev => [...prev, child.id]);
|
checked ? [...prev, child.id] : prev.filter(id => id !== child.id)
|
||||||
} else {
|
);
|
||||||
setSelectedChildIds(prev => 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>
|
</label>
|
||||||
))}
|
))}
|
||||||
|
<p>
|
||||||
<p style={{ marginTop: '10px' }}>
|
<strong>Total Harga:</strong> Rp {selectedChildIds
|
||||||
<strong>Total Harga:</strong>{' '}
|
.map(id => product.children.find(child => child.id === id)?.price || 0)
|
||||||
Rp {selectedChildIds
|
|
||||||
.map(id => {
|
|
||||||
const found = product.children.find(child => child.id === id);
|
|
||||||
return found ? found.price || 0 : 0;
|
|
||||||
})
|
|
||||||
.reduce((a, b) => a + b, 0)
|
.reduce((a, b) => a + b, 0)
|
||||||
.toLocaleString('id-ID')}
|
.toLocaleString('id-ID')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
|
<button className={styles.button} onClick={() => setShowChildSelector(false)}>
|
||||||
<button
|
|
||||||
className={`${styles.button} ${styles.cancelButton}`}
|
|
||||||
onClick={() => {
|
|
||||||
setShowChildSelector(false);
|
|
||||||
setSelectedChildIds([]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Kembali
|
Kembali
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className={styles.button} onClick={onConfirmChildren}>
|
||||||
className={`${styles.button} ${styles.confirmButton}`}
|
|
||||||
onClick={onConfirmChildren}
|
|
||||||
>
|
|
||||||
Lanjut ke Checkout
|
Lanjut ke Checkout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -542,9 +542,88 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 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) {
|
@media (max-width: 600px) {
|
||||||
.nav {
|
.nav {
|
||||||
display: none;
|
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 Login from '../Login';
|
||||||
import styles from '../Styles.module.css';
|
import styles from '../Styles.module.css';
|
||||||
|
|
||||||
|
const CoursePage = ({ subscriptions }) => {
|
||||||
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 [postLoginAction, setPostLoginAction] = useState(null);
|
const [postLoginAction, setPostLoginAction] = useState(null);
|
||||||
const [selectedProduct, setSelectedProduct] = useState({});
|
const [selectedProduct, setSelectedProduct] = useState({});
|
||||||
const [hoveredCard, setHoveredCard] = useState(null);
|
const [hoveredCard, setHoveredCard] = useState(null);
|
||||||
const [showedModal, setShowedModal] = useState(null);
|
const [showedModal, setShowedModal] = useState(null);
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
if (!subscriptions) return;
|
||||||
if (match) {
|
|
||||||
const token = match[2];
|
|
||||||
|
|
||||||
const productIds = getDistinctProductIdsFromJwt(token);
|
// Step 1: Group subscriptions by product_name
|
||||||
const endDates = getLatestEndDatesFromJwt(token);
|
function groupSubscriptionsByProductName(subs) {
|
||||||
const tokenQuantitiesFromJwt = getTotalTokenFromJwt(token);
|
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', {
|
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -84,57 +66,33 @@ useEffect(() => {
|
|||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const parentMap = {};
|
const enrichedData = Object.values(groupedSubs).map(group => {
|
||||||
const childrenMap = {};
|
const productData = data.find(p => p.id == group.product_id);
|
||||||
|
|
||||||
data.forEach(product => {
|
return {
|
||||||
if (product.sub_product_of) {
|
id: group.product_id,
|
||||||
const parentId = product.sub_product_of;
|
name: group.product_name,
|
||||||
if (!childrenMap[parentId]) childrenMap[parentId] = [];
|
type: productData?.type || 'product',
|
||||||
childrenMap[parentId].push(product);
|
image: productData?.image || '',
|
||||||
} else {
|
description: productData?.description || '',
|
||||||
parentMap[product.id] = {
|
price: productData?.price || 0,
|
||||||
...product,
|
currency: productData?.currency || 'IDR',
|
||||||
quantity: product.quantity || 0,
|
duration: productData?.duration || {},
|
||||||
end_date: endDates[product.id] || null,
|
sub_product_of: productData?.sub_product_of || null,
|
||||||
children: []
|
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
|
||||||
};
|
};
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// ...
|
console.log(enrichedData)
|
||||||
|
|
||||||
Object.keys(childrenMap).forEach(parentId => {
|
|
||||||
const parent = parentMap[parentId];
|
|
||||||
const children = childrenMap[parentId];
|
|
||||||
|
|
||||||
if (parent) {
|
|
||||||
parent.children = children;
|
|
||||||
|
|
||||||
// Pakai quantity dari JWT langsung (tokenQuantitiesFromJwt)
|
|
||||||
parent.quantity = children.reduce((total, child) => {
|
|
||||||
return total + (tokenQuantitiesFromJwt[child.id] || 0);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// Update quantity untuk produk yang bukan parent dan bukan anak
|
|
||||||
Object.values(parentMap).forEach(product => {
|
|
||||||
if (!product.children.length) {
|
|
||||||
if (product.unit_type === 'token') {
|
|
||||||
product.quantity = tokenQuantitiesFromJwt[product.id] || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const enrichedData = Object.values(parentMap);
|
|
||||||
setProducts(enrichedData);
|
setProducts(enrichedData);
|
||||||
console.log(enrichedData);
|
console.log('Enriched Data:', enrichedData);
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Fetch error:', err));
|
.catch(err => console.error('Fetch error:', err));
|
||||||
}
|
}, [subscriptions]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
@@ -171,19 +129,19 @@ Object.values(parentMap).forEach(product => {
|
|||||||
products
|
products
|
||||||
.map(product => (
|
.map(product => (
|
||||||
<div
|
<div
|
||||||
key={product.id}
|
key={product.name}
|
||||||
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
|
className={`${styles.courseCard} ${hoveredCard === product.name ? styles.courseCardHover : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedProduct(product);
|
setSelectedProduct(product);
|
||||||
setShowedModal('product');
|
setShowedModal('product');
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => setHoveredCard(product.id)}
|
onMouseEnter={() => setHoveredCard(product.name)}
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
>
|
>
|
||||||
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
|
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
|
||||||
{product.price == 0 && (
|
{/* {product.price == 0 && (
|
||||||
<span className={styles.courseLabel}>Free</span>
|
<span className={styles.courseLabel}>Free</span>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.courseContent}>
|
<div className={styles.courseContent}>
|
||||||
<h3 className={styles.courseTitle}>{product.name}</h3>
|
<h3 className={styles.courseTitle}>{product.name}</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user