diff --git a/package-lock.json b/package-lock.json index 61eb764..0114192 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", "bootstrap": "^5.3.7", + "lucide-react": "^0.536.0", "react": "^19.1.1", "react-bootstrap": "^2.10.10", "react-dom": "^19.1.1", @@ -11408,6 +11409,15 @@ "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": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/package.json b/package.json index 9dfe3d5..29e68d7 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", "bootstrap": "^5.3.7", + "lucide-react": "^0.536.0", "react": "^19.1.1", "react-bootstrap": "^2.10.10", "react-dom": "^19.1.1", diff --git a/src/App.js b/src/App.js index 216245c..27f70ef 100644 --- a/src/App.js +++ b/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} /> - '%' + ('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 (
@@ -150,7 +165,7 @@ function App() { return (
-
+
+ + } + /> + } />
- {/* Unified Modal */} - {showedModal && ( -
{ - setShowedModal(null); - setSelectedProduct({}); - }} - > -
e.stopPropagation()} - > - {showedModal === 'product' && ( - { - setShowedModal(null); - setSelectedProduct({}); - }} - /> - )} - {showedModal === 'login' && ( - setShowedModal(null)} /> - )} -
-
- )} + {/* Unified Modal */} + {showedModal && ( +
{ + setShowedModal(null); + setSelectedProduct({}); + }} + > +
e.stopPropagation()} + > + {showedModal === 'product' && ( + { + setShowedModal(null); + setSelectedProduct({}); + }} + /> + )} + {showedModal === 'login' && ( + setShowedModal(null)} /> + )} +
+
+ )}
); diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js new file mode 100644 index 0000000..1db87e6 --- /dev/null +++ b/src/components/Dashboard.js @@ -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 }) => ( +
+
+

{title}

+ +
+ +
+ {currency && `${currency} `}{formatCurrency(value)} +
+ +
+
+ {isNegative ? ( + + ) : ( + + )} + + {Math.abs(change)}% + + from last week +
+
+ +
{period}
+
+ ); + + const BarChart = ({ data }) => { + const maxValue = Math.max(...data.map(item => Math.max(item.items, item.revenue))); + + return ( +
+ {data.map((item, index) => ( +
+
+
+
+
+ {item.date} +
+ ))} +
+ ); + }; + + return ( +
+ {/* Stats Cards */} +
+ + + +
+ + {/* Charts and Transactions */} +
+ {/* Report Statistics */} +
+
+
+

Report Statistics

+

Period: 22 - 29 May 2025

+
+
+
+
+ Items Sold +
+
+
+ Revenue +
+
+
+ +
+ + {/* Latest Transactions */} +
+
+

Latest Transactions

+ see all transactions +
+ +
+ {dashboardData.latestTransactions.map((transaction) => ( +
+
+
+ {transaction.avatar} +
+
+

{transaction.name}

+

on {transaction.date}

+
+
+ +
+ + IDR {formatCurrency(transaction.amount)} + +
+ + {transaction.status} + +
+
+ ))} +
+
+
+
+ ); +}; + +export default Dashboard; diff --git a/src/components/Dashboard.module.css b/src/components/Dashboard.module.css new file mode 100644 index 0000000..963c2ad --- /dev/null +++ b/src/components/Dashboard.module.css @@ -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; +} \ No newline at end of file diff --git a/src/components/Header.js b/src/components/Header.js index 083f4d7..1081677 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -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 ( -
Logo + {/* Desktop Navigation */} + {/* Burger Menu Button */} +
setMenuOpen(!menuOpen)}> + ☰ +
+ + {/* Mobile Dropdown Menu */} + {menuOpen && ( +
+ {username ? ( + <> +
Halo, {username}
+ + + + + + ) : ( + + )} +
+ )} + + {/* Desktop Auth Buttons */}
- {username ? ( - - Halo, {username} - - ) : ( + {username && ( +
+ Halo, {username} + +
+ )} + {!username && ( @@ -67,4 +98,4 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal }) = ); }; -export default Header; \ No newline at end of file +export default Header; diff --git a/src/components/ProductDetailPage.js b/src/components/ProductDetailPage.js index 9520b30..acd564c 100644 --- a/src/components/ProductDetailPage.js +++ b/src/components/ProductDetailPage.js @@ -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 (
- - {/* ✅ Tampilan utama disembunyikan jika sedang memilih child */} - {!showChildSelector && ( + {!showChildSelector && !showSubscriptionSelector && !showNamingInput && ( <> -
- +

{product.name}

- {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')}`}
-

{product.description}

-
- - -
)} - {/* ✅ UI pemilihan child */} {showChildSelector && (

Pilih Paket

{product.children.map(child => ( -
)} + + {showSubscriptionSelector && !showNamingInput && ( +
+
Perpanjang {product.name}
+ {matchingSubscriptions.map(sub => ( + + ))} +
Atau buat baru
+ +
+ + +
+
+ )} + +{showNamingInput && ( +
+
Buat {product.name} Baru
+ setCustomName(e.target.value)} + style={{ width: '100%', padding: '8px', marginBottom: '16px', borderRadius: '10px' }} + /> + + { + matchingSubscriptions.some( + (sub) => sub.product_name === `${product.name}@${customName}` + ) && ( +

+ Nama produk sudah digunakan. +

+ ) + } + +
+ + +
+
+)} +
); }; diff --git a/src/components/Styles.module.css b/src/components/Styles.module.css index 388d74b..c3c6302 100644 --- a/src/components/Styles.module.css +++ b/src/components/Styles.module.css @@ -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; } diff --git a/src/components/pages/ProductsPage.js b/src/components/pages/ProductsPage.js index f544097..22504e2 100644 --- a/src/components/pages/ProductsPage.js +++ b/src/components/pages/ProductsPage.js @@ -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 => (
{ setSelectedProduct(product); setShowedModal('product'); }} - onMouseEnter={() => setHoveredCard(product.id)} + onMouseEnter={() => setHoveredCard(product.name)} onMouseLeave={() => setHoveredCard(null)} >
- {product.price == 0 && ( + {/* {product.price == 0 && ( Free - )} + )} */}

{product.name}