Compare commits

...

10 Commits

Author SHA1 Message Date
Vassshhh
32cff2df1f ok 2025-08-13 17:28:38 +07:00
Vassshhh
9639d6c2de ok 2025-08-12 18:11:31 +07:00
Vassshhh
8fd2661cf6 ok 2025-08-12 10:58:45 +07:00
Vassshhh
f577a23d10 ok 2025-08-12 02:31:50 +07:00
Vassshhh
82518c96aa ok 2025-08-11 16:38:35 +07:00
everythingonblack
7d3655236e ok 2025-08-07 20:02:33 +07:00
everythingonblack
3b7707a024 ok 2025-08-07 17:04:22 +07:00
Vassshhh
3cf86829ed ok 2025-08-07 15:47:34 +07:00
Vassshhh
f2b30f515c ok 2025-08-06 17:04:30 +07:00
Vassshhh
59ab35c332 ok 2025-08-06 12:24:59 +07:00
20 changed files with 1210 additions and 714 deletions

View File

@@ -2,9 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import styles from './components/Styles.module.css';
// Import components
import Login from './components/Login';
import Header from './components/Header';
import HeroSection from './components/HeroSection';
import ServicesSection from './components/ServicesSection';
@@ -14,13 +12,12 @@ import AboutUsSection from './components/AboutUsSection';
import KnowledgeBaseSection from './components/KnowledgeBaseSection';
import ClientsSection from './components/ClientsSection';
import Footer from './components/Footer';
import ProductDetailPage from './components/ProductDetailPage';
import Dashboard from './components/Dashboard';
import CreateProductPage from './components/CreateProductPage';
import ProductsPage from './components/pages/ProductsPage';
import processProducts from './helper/processProducts';
function HomePage({
hoveredCard,
@@ -32,10 +29,11 @@ function HomePage({
productSectionRef,
courseSectionRef
}) {
return (
<>
<HeroSection />
<AboutUsSection />
<ServicesSection />
<ProductSection
productSectionRef={productSectionRef}
@@ -49,13 +47,14 @@ function HomePage({
hoveredCard={hoveredCard}
setHoveredCard={setHoveredCard}
setSelectedProduct={setSelectedProduct}
setShowedModal={setShowedModal} />
<AboutUsSection />
setShowedModal={setShowedModal}
/>
<KnowledgeBaseSection />
<ClientsSection />
</>
);
}
function parseJwt(token) {
try {
const base64Url = token.split('.')[1];
@@ -74,60 +73,188 @@ function parseJwt(token) {
function App() {
const [loading, setLoading] = useState(true);
// 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 [showedModal, setShowedModal] = useState(null);
const [subProductOf, setSubProductOf] = useState(null);
const [username, setUsername] = useState(null);
const productSectionRef = useRef(null);
const courseSectionRef = useRef(null);
const [productModalRequest, setProductModalRequest] = useState(null);
const scrollToProduct = () => {
productSectionRef.current?.scrollIntoView({ behavior: "smooth" });
};
const scrollToCourse = () => {
courseSectionRef.current?.scrollIntoView({ behavior: "smooth" });
};
const requestLogin = (nextAction) => {
const url = new URL(window.location);
url.searchParams.set('next', nextAction);
window.history.pushState({}, '', url);
setShowedModal('login');
};
// Ambil token dan user info dari cookie
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', {
fetch('https://bot.kediritechnopark.com/webhook/user-production/data', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
})
.then(res => res.json())
.then(data => {
if (data && data.token) {
// Update token with data[0].token
document.cookie = `token=${data.token}; path=/`;
console.log(data)
setSubscriptions(data.subscriptions)
setSubscriptions(data.subscriptions);
const payload = parseJwt(data.token);
if (payload && payload.username) {
setUsername(payload.username);
}
} else {
console.warn('Token tidak ditemukan dalam data.');
}
})
.catch(err => console.error('Fetch error:', err));
.catch(err => {
document.cookie = 'token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
setUsername(null);
window.location.reload();
});
}
}, []);
const scrollToProduct = () => {
productSectionRef.current?.scrollIntoView({ behavior: "smooth" });
};
const scrollToCourse = () => {
courseSectionRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const modalType = params.get('modal');
const productId = params.get('product_id');
const authorizedUri = params.get('authorized_uri');
const unauthorizedUri = params.get('unauthorized_uri');
if (modalType === 'product' && productId) {
const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2];
// Simpan semua param penting ke localStorage
localStorage.setItem('product_id', productId);
if (authorizedUri) localStorage.setItem('authorized_uri', authorizedUri);
if (unauthorizedUri) localStorage.setItem('unauthorized_uri', unauthorizedUri);
// Jika belum login, tampilkan modal login
if (!token) {
setShowedModal('login');
}
// Jika sudah login, tidak langsung fetch di sini — akan diproses saat subscriptions tersedia
}
}, []);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const modalType = params.get('modal');
const productId = parseInt(params.get('product_id'));
const authorizedUri = params.get('authorized_uri');
const unauthorizedUri = params.get('unauthorized_uri');
const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2];
if (modalType === 'product' && productId) {
if (!token) {
setShowedModal('login'); // belum login → tampilkan login modal
} else {
// sudah login → lanjutkan proses otorisasi saat subscriptions tersedia
setProductModalRequest({ productId, authorizedUri, unauthorizedUri });
console.log('modal')
}
}
}, []);
useEffect(() => {
console.log(subscriptions)
if (!productModalRequest || !subscriptions) return;
const { productId, authorizedUri, unauthorizedUri } = productModalRequest;
console.log(subscriptions)
const hasAccess = subscriptions && subscriptions.some(
sub => sub.product_id === productId || sub.product_parent_id === productId
);
console.log(hasAccess)
if (hasAccess) {
if (authorizedUri) {
let finalUri = decodeURIComponent(authorizedUri);
const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2];
if (finalUri.includes('token=null') || finalUri.includes('token=')) {
const url = new URL(finalUri);
url.searchParams.set('token', token || '');
finalUri = url.toString();
}
window.location.href = finalUri;
}
else {// Assuming you already imported processProducts from './processProducts'
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
itemsId: [productId],
withChildren: true,
}),
})
.then(res => res.json())
.then(data => {
if (Array.isArray(data) && data.length > 0) {
// Process the raw data to group children under their parent
const processed = processProducts(data);
// Set the first product (which should be the parent with children nested)
setSelectedProduct(processed[0]);
setShowedModal('product');
}
})
.catch(err => console.error('Fetch product error:', err));
}
} else {
if (unauthorizedUri) {
window.location.href = decodeURIComponent(unauthorizedUri);
} else {
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
itemsId: [productId],
withChildren: true,
}),
})
.then(res => res.json())
.then(data => {
if (Array.isArray(data) && data.length > 0) {
// Process the raw data to group children under their parent
const processed = processProducts(data);
// Set the first product (which should be the parent with children nested)
setSelectedProduct(processed[0]);
setShowedModal('product');
}
})
.catch(err => console.error('Fetch product error:', err));
console.log('modal')
}
}
setProductModalRequest(null); // reset
}, [subscriptions, productModalRequest]);
useEffect(() => {
const timer = setTimeout(() => {
@@ -137,13 +264,8 @@ function App() {
}, []);
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
setUsername(null);
window.location.reload();
};
@@ -165,7 +287,13 @@ function App() {
return (
<Router>
<div className="App">
<Header username={username} scrollToProduct={scrollToProduct} scrollToCourse={scrollToCourse} setShowedModal={setShowedModal} handleLogout={handleLogout} />
<Header
username={username}
scrollToProduct={scrollToProduct}
scrollToCourse={scrollToCourse}
setShowedModal={setShowedModal}
handleLogout={handleLogout}
/>
<Routes>
<Route
path="/"
@@ -182,47 +310,57 @@ function App() {
/>
}
/>
<Route
path="/products"
element={
<ProductsPage subscriptions={subscriptions}/>
}
/>
<Route path="/products" element={<ProductsPage subscriptions={subscriptions} />} />
<Route
path="/dashboard"
element={
<Dashboard />
<Dashboard
setShowedModal={(e, productId) => {
setShowedModal(e);
setSubProductOf(productId);
}}
/>
}
/>
</Routes>
<Footer />
{/* Unified Modal */}
{/* Modal */}
{showedModal && (
<div
className={styles.modal}
onClick={() => {
const url = new URL(window.location);
url.searchParams.delete('modal');
url.searchParams.delete('product_id');
url.searchParams.delete('authorized_uri');
url.searchParams.delete('unauthorized_uri');
url.searchParams.delete('next');
window.history.pushState({}, '', url);
setShowedModal(null);
setSelectedProduct({});
}}
>
<div
className={styles.modalBody}
onClick={(e) => e.stopPropagation()}
>
<div className={styles.modalBody} onClick={(e) => e.stopPropagation()}>
{showedModal === 'product' && (
<ProductDetailPage
subscriptions={subscriptions}
setPostLoginAction={setPostLoginAction}
setShowedModal={setShowedModal}
requestLogin={requestLogin}
product={selectedProduct}
onClose={() => {
setShowedModal(null);
setSelectedProduct({});
}}
setShowedModal={setShowedModal}
/>
)}
{showedModal === 'create-item' && (
<CreateProductPage
parentId={subProductOf}
subscriptions={subscriptions}
requestLogin={requestLogin}
product={selectedProduct}
setShowedModal={setShowedModal}
/>
)}
{showedModal === 'login' && (
<Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} />
<Login setShowedModal={setShowedModal} />
)}
</div>
</div>

View File

@@ -17,8 +17,8 @@ const AboutUsSection = () => {
Dengan misi memberdayakan talenta lokal, menjembatani teknologi dan industri, serta mempercepat transformasi digital, Kediri Technopark berkomitmen menjadi penggerak kemajuan ekonomi dan teknologi, baik di tingkat lokal maupun nasional.
</p>
<div className="mt-4 d-flex gap-3">
<Button href="konsultasi.html" className="px-4 py-2 rounded-pill text-white" style={{ background: 'linear-gradient(to right, #6a59ff, #8261ee)', border: 'none' }}>
Konsultasi
<Button href="https://instagram.com/kediri.technopark" className="px-4 py-2 rounded-pill text-white" style={{ background: 'linear-gradient(to right, #6a59ff, #8261ee)', border: 'none' }}>
Instagram
</Button>
<Button href="https://wa.me/6281318894994" target="_blank" variant="outline-success" className="px-4 py-2 rounded-pill">
<i className="fab fa-whatsapp"></i> WhatsApp

View File

@@ -6,7 +6,7 @@ const AcademySection = ({hoveredCard, setHoveredCard, setSelectedProduct, setSho
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -20,17 +20,19 @@ const AcademySection = ({hoveredCard, setHoveredCard, setSelectedProduct, setSho
return (
<section id="services" className="services pt-5">
<section id="services" className="services pt-5" ref={courseSectionRef}>
<Container>
<div className="section-heading text-center mb-4">
<div className="section-heading mb-4">
<h4>OUR <em>ACADEMY PROGRAM</em></h4>
<img src="/assets/images/heading-line-dec.png" alt="" />
<p>Academy Program adalah wadah belajar digital untuk anak-anak dan remaja. Didesain interaktif, kreatif, dan gratis setiap modul membekali peserta dengan keterampilan masa depan, dari teknologi dasar hingga coding dan proyek nyata.</p>
</div>
<div className={styles.coursesGrid}>
{products &&
products[0]?.name &&
products.map(product => (
products
.map(product => (
<div
key={product.id}
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
@@ -41,14 +43,18 @@ const AcademySection = ({hoveredCard, setHoveredCard, setSelectedProduct, setSho
onMouseEnter={() => setHoveredCard(product.id)}
onMouseLeave={() => setHoveredCard(null)}
>
<div>
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
{product.price === 0 && (
<span className={styles.courseLabel}>Free</span>
)}
</div>
<div className={styles.courseContent}>
<div className={styles.courseContentTop}>
<h3 className={styles.courseTitle}>{product.name}</h3>
<p className={styles.courseDesc}>{product.description}</p>
</div>
</div>
<div className={styles.courseContentBottom}>
<div className={styles.coursePrice}>
<span
className={

View File

@@ -1,23 +1,35 @@
import React from 'react';
import { Container, Row, Col, Image } from 'react-bootstrap';
import styles from './Styles.module.css';
const ClientsSection = () => {
const logos = [
'dermalounge.jpg',
'suar.avif',
'kloowear.png',
'psi.png',
];
return (
<section id="clients" className="the-clients section pt-5">
<section id="clients" className="the-clients section py-5">
<Container>
<Row>
<Col lg={{ span: 8, offset: 2 }}>
<div className="section-heading text-center mb-4">
<Col>
<div className="section-heading mb-4">
<h4>TRUSTED BY <em>OUR CLIENTS</em></h4>
<img src="/assets/images/heading-line-dec.png" alt="" className="mb-3" />
<p>We are proud to work with these amazing brands and organizations.</p>
</div>
<div id="clients-carousel" className="d-flex justify-content-center flex-wrap">
{[1, 2, 3, 4, 5].map((num) => (
<div key={num} className="client-logo-wrapper m-2">
<Image src={`/assets/images/client-logo${num}.png`} alt={`Client ${num}`} fluid />
<div id="clients-carousel" className="d-flex justify-content-left flex-wrap">
{logos.map((logo, index) => (
<div className={`${styles.clientLogoWrapper} m-2`} key={index}>
<Image
src={`https://kediritechnopark.com/assets/${logo}`}
fluid
className={styles.clientLogo}
/>
</div>
))}
</div>
))}</div>
</Col>
</Row>
</Container>

View File

@@ -0,0 +1,239 @@
import React, { useEffect, useState } from 'react';
import styles from './Dashboard.module.css';
const CreateProductPage = ({ parentId = null, onSuccess, onCancel }) => {
const [availableTypes, setAvailableTypes] = useState([]);
const [availableGroups, setAvailableGroups] = useState([]);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [image, setImage] = useState('');
const [price, setPrice] = useState('');
const [unitType, setUnitType] = useState('duration');
const [durationValue, setDurationValue] = useState('');
const [durationUnit, setDurationUnit] = useState('days');
const [quantity, setQuantity] = useState('');
const [selectedType, setSelectedType] = useState('');
const [selectedGroup, setSelectedGroup] = useState('');
const [siteUrl, setSiteUrl] = useState('');
const [createUpdateUrl, setCreateUpdateUrl] = useState('');
const [isVisible, setIsVisible] = useState(true);
const [step, setStep] = useState(0);
useEffect(() => {
const fetchDistinctOptions = async () => {
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
if (!match) return;
const token = match[2];
try {
const res = await fetch(
'https://bot.kediritechnopark.com/webhook/store-production/get-products',
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
}
);
const result = await res.json();
const productsArr = result || [];
const types = [...new Set(productsArr.map((p) => p.type).filter(Boolean))];
const groups = [...new Set(productsArr.map((p) => p.group).filter(Boolean))];
setAvailableTypes(types);
setAvailableGroups(groups);
} catch (err) {
console.error('Gagal ambil produk:', err);
}
};
fetchDistinctOptions();
}, []);
const sendDataToN8N = async () => {
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
if (!match) {
alert('Token tidak ditemukan. Silakan login kembali.');
return;
}
const token = match[2];
const isToken = unitType === 'token';
const payload = {
name,
type: selectedType,
image,
description,
price: price === '' ? null : parseInt(price, 10),
currency: 'IDR',
duration: isToken ? null : `${parseInt(durationValue || '0', 10)} ${durationUnit}`,
quantity: isToken ? parseInt(quantity || '0', 10) : null,
unit_type: unitType,
sub_product_of: parentId,
is_visible: isVisible,
group: selectedGroup || null,
site_url: siteUrl || null,
create_update_url: createUpdateUrl || null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
try {
const response = await fetch(
'https://bot.kediritechnopark.com/webhook/store-production/add-product',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
}
);
if (response.ok) {
alert('Produk berhasil ditambahkan!');
if (onSuccess) onSuccess();
} else {
const errorText = await response.text();
console.error('Response Error:', errorText);
alert('Gagal mengirim data: ' + response.status);
}
} catch (error) {
console.error('Error sending data to n8n:', error);
alert('Terjadi kesalahan saat mengirim data.');
}
};
return (
<div className={styles.chartCard}>
<h3 className={styles.transactionsTitle}>
{parentId ? 'Tambah Sub-Produk' : 'Tambah Produk Baru'}
</h3>
{step === 0 && (
<section className={styles.formSection}>
<div className={styles.formGroup}>
<label>Nama Produk</label>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
</div>
<div className={styles.formGroup}>
<label>Deskripsi</label>
<textarea rows={3} value={description} onChange={(e) => setDescription(e.target.value)} />
</div>
<div className={styles.formGroup}>
<label>URL Gambar</label>
<input type="text" value={image} onChange={(e) => setImage(e.target.value)} />
</div>
</section>
)}
{step === 1 && (
<section className={styles.formSection}>
<div className={styles.formGroup}>
<label>Harga</label>
<input type="number" value={price} onChange={(e) => setPrice(e.target.value)} />
</div>
<div className={styles.formGroup}>
<label>Jenis Unit</label>
<select value={unitType} onChange={(e) => setUnitType(e.target.value)}>
<option value="duration">Durasi</option>
<option value="token">Token</option>
</select>
</div>
{unitType === 'token' ? (
<div className={styles.formGroup}>
<label>Jumlah Token</label>
<input type="number" value={quantity} onChange={(e) => setQuantity(e.target.value)} />
</div>
) : (
<div className={styles.formGroup}>
<label>Durasi</label>
<div style={{ display: 'flex', gap: '0.5rem', width: '100%' }}>
<input type="number" style={{ width: '100%' }} value={durationValue} onChange={(e) => setDurationValue(e.target.value)} />
<select value={durationUnit} onChange={(e) => setDurationUnit(e.target.value)}>
<option value="days">Hari</option>
<option value="weeks">Minggu</option>
<option value="months">Bulan</option>
</select>
</div>
</div>
)}
<div className={styles.formGroup} style={{ display: 'flex', flexDirection: 'row', gap: '0.5rem' }}>
<input id="visible" type="checkbox" checked={isVisible} onChange={(e) => setIsVisible(e.target.checked)} />
<label htmlFor="visible" style={{ margin: 0 }}>Tampilkan produk</label>
</div>
</section>
)}
{step === 2 && (
<section className={styles.formSection}>
<div className={styles.formGroup}>
<label>Tipe Produk</label>
<input type="text" value={selectedType} onChange={(e) => setSelectedType(e.target.value)} />
<div className={styles.suggestionContainer}>
{availableTypes.map((type) => (
<button
key={type}
type="button"
className={styles.suggestionButton}
onClick={() => setSelectedType(type)}
>
{type}
</button>
))}
</div>
</div>
<div className={styles.formGroup}>
<label>Group</label>
<input type="text" value={selectedGroup} onChange={(e) => setSelectedGroup(e.target.value)} />
<div className={styles.suggestionContainer}>
{availableGroups.map((group) => (
<button
key={group}
type="button"
className={styles.suggestionButton}
onClick={() => setSelectedGroup(group)}
>
{group}
</button>
))}
</div>
</div>
<div className={styles.formGroup}>
<label>Site URL</label>
<input type="text" value={siteUrl} onChange={(e) => setSiteUrl(e.target.value)} />
</div>
<div className={styles.formGroup}>
<label>CREATE UPDATE URL</label>
<input type="text" value={createUpdateUrl} onChange={(e) => setCreateUpdateUrl(e.target.value)} />
</div>
</section>
)}
<div className={styles.formActions}>
<button type="button" className={styles.submitButton} style={{visibility: step < 1 ? 'hidden': 'visible' }} onClick={() => setStep((s) => s - 1)}>
Back
</button>
{step < 2 ? (
<button type="button" className={styles.submitButton} onClick={() => setStep((s) => s + 1)}>
Next
</button>
) : (
<button type="button" className={styles.submitButton} onClick={sendDataToN8N}>
{parentId ? 'Buat Sub-Produk' : 'Buat Produk'}
</button>
)}
</div>
</div>
);
};
export default CreateProductPage;

View File

@@ -1,130 +1,77 @@
import React, { useState, useEffect } from 'react';
import { TrendingUp, TrendingDown, DollarSign, ShoppingCart, Users } from 'lucide-react';
import { TrendingUp, TrendingDown, DollarSign, ShoppingCart, Users, Plus, GitBranchPlus } from 'lucide-react';
import styles from './Dashboard.module.css';
import processProducts from '../helper/processProducts';
/**
* Props:
* - setShowedModal: (modalName: string, productId?: string|number) => void
*/
const Dashboard = ({ setShowedModal }) => {
const [unitType, setUnitType] = useState('duration'); // kept for potential reuse
const [durationUnit, setDurationUnit] = useState('days'); // kept for potential reuse
const [availableTypes, setAvailableTypes] = useState([]);
const [availableGroups, setAvailableGroups] = useState([]);
const [selectedType, setSelectedType] = useState(null);
const [selectedGroup, setSelectedGroup] = useState(null);
const [isVisible, setIsVisible] = useState(true);
const [products, setProducts] = useState([]);
const 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'
},
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 }
{ date: '25/06', items: 300, revenue: 350 },
{ date: '26/06', items: 900, revenue: 450 },
{ date: '27/06', items: 550, revenue: 200 },
],
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'
}
]
latestTransactions: []
});
// Function untuk connect ke n8n webhook
const connectToN8NWebhook = async (webhookUrl) => {
useEffect(() => {
const fetchDistinctOptions = async () => {
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
if (!match) return;
const token = match[2];
try {
const response = await fetch(webhookUrl, {
const res = await fetch('https://bot.kediritechnopark.com/webhook/store-production/get-products', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setDashboardData(data);
}
} catch (error) {
console.error('Error connecting to n8n webhook:', error);
const result = await res.json();
const productsArr = result || [];
const types = [...new Set(productsArr.map(p => p.type).filter(Boolean))];
const groups = [...new Set(productsArr.map(p => p.group).filter(Boolean))];
setAvailableTypes(types);
setAvailableGroups(groups);
setProducts(processProducts(productsArr));
} catch (err) {
console.error('Gagal ambil produk:', err);
}
};
// 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),
});
fetchDistinctOptions();
}, []);
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 formatCurrency = (amount) => 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;
case 'confirmed': return styles.statusConfirmed;
case 'waiting payment': return styles.statusWaiting;
case 'payment expired': return styles.statusExpired;
default: return styles.statusConfirmed;
}
};
@@ -134,11 +81,9 @@ const Dashboard = () => {
<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 ? (
@@ -152,31 +97,19 @@ const Dashboard = () => {
<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 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>
@@ -187,92 +120,70 @@ const Dashboard = () => {
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}
/>
<StatCard title="Total Revenue" value={dashboardData.totalRevenue.amount} currency="IDR" 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} />
{/* Tempatkan <BarChart data={dashboardData.chartData} /> jika mau ditampilkan */}
</div>
{/* Latest Transactions */}
{/* Products List */}
<div className={styles.chartCard}>
<div className={styles.transactionsHeader}>
<h3 className={styles.transactionsTitle}>Latest Transactions</h3>
<a href="#" className={styles.seeAllLink}>see all transactions</a>
<h3 className={styles.transactionsTitle}>Products</h3>
{/* Tombol "Buat Item" → buka modal create */}
<button
type="button"
className={styles.primaryButton}
onClick={() => setShowedModal && setShowedModal('create-item')}
title="Buat produk baru"
>
<Plus size={16} style={{ marginRight: 6 }} />
Buat Item
</button>
</div>
<div className={styles.transactionsList}>
{dashboardData.latestTransactions.map((transaction) => (
<div key={transaction.id} className={styles.transactionItem}>
{products.map((product) => (
<div key={product.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>
<h4>{product.name}</h4>
{product.children && product.children.map((child) => (
<p key={child.id}>- {child.name}</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}
IDR {formatCurrency(product.price)}
</span>
<div className={`${styles.statusIndicator} ${getStatusClass(product.status)}`}></div>
<span className={styles.transactionStatus}>{product.status}</span>
{/* Tombol "Add Child" → buka modal create dengan parent product_id */}
<button
type="button"
className={styles.secondaryButton}
onClick={() => setShowedModal('create-item', product.id)}
title="Tambah sub-produk"
style={{ marginLeft: '0.75rem' }}
>
<GitBranchPlus size={16} style={{ marginRight: 6 }} />
Add Child
</button>
</div>
</div>
))}
</div>
</div>
</div>
{/* Bagian form create yang lama sudah DIPINDAH ke halaman/komponen baru */}
</div>
);
};

View File

@@ -117,6 +117,9 @@
}
.chartCard {
width: 100%;
color: black;
text-align: left;
background: white;
border-radius: 12px;
padding: 24px;
@@ -225,7 +228,6 @@
font-size: 18px;
font-weight: 600;
color: #111827;
margin: 0;
}
.seeAllLink {
@@ -324,3 +326,70 @@
color: #6b7280;
text-transform: capitalize;
}
.form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.formGroup {
display: flex;
flex-direction: column;
}
.formGroup label {
font-weight: 500;
margin-bottom: 0.5rem;
}
.formGroup input,
.formGroup textarea {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 0.5rem;
}
.submitButton {
background-color: #2563eb;
color: white;
padding: 0.6rem 1rem;
border: none;
border-radius: 0.6rem;
cursor: pointer;
font-weight: 600;
}
.formGroup select {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 0.5rem;
}
.suggestionContainer {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.suggestionButton {
background-color: #eee;
border: none;
padding: 0.25rem 0.75rem;
border-radius: 12px;
cursor: pointer;
font-size: 0.85rem;
transition: background-color 0.2s;
}
.suggestionButton:hover {
background-color: #ccc;
}
.formActions {
margin-top: 10px;
display: flex;
justify-content: space-between;
}

View File

@@ -1,43 +1,33 @@
import React from 'react';
import { Container, Row, Col } from 'react-bootstrap';
import styles from './Styles.module.css';
const Footer = () => {
return (
<footer id="contact" className="bg-dark text-white py-4">
<footer id="contact" className={`bg-dark text-white py-4 ${styles.footer}`}>
<Container>
<Row className="justify-content-center">
<Col lg={6} className="text-center mb-3">
<Row className="justify-content-center text-start">
<Col lg={6} className="mb-3">
<h4>Contact Us</h4>
<p>Sunan Giri GG. I No. 11, Rejomulyo, Kediri, Jawa Timur 64129</p>
<p><a href="tel:+6281318894994" className="text-white">0813 1889 4994</a></p>
<p><a href="mailto:marketing@kediritechnopark.com" className="text-white">marketing@kediritechnopark.com</a></p>
<p><a href="https://instagram.com/kediri.technopark" target="_blank" rel="noopener noreferrer" className="text-white">@kediri.technopark</a></p>
<p><a href="https://kediritechnopark.com" target="_blank" rel="noopener noreferrer" className="text-white">www.KEDIRITECHNOPARK.com</a></p>
<div className="mt-3">
<a href="https://wa.me/6281318894994" target="_blank" rel="noopener noreferrer" className="me-3 text-white fs-4">
<i className="fab fa-whatsapp"></i>
</a>
<a href="https://instagram.com/kediri.technopark" target="_blank" rel="noopener noreferrer" className="text-white fs-4">
<i className="fab fa-instagram"></i>
</a>
</div>
<p><a href="tel:+6281318894994">0813 1889 4994</a></p>
<p><a href="mailto:marketing@kediritechnopark.com">marketing@kediritechnopark.com</a></p>
<p><a href="https://instagram.com/kediri.technopark" target="_blank" rel="noopener noreferrer">@kediri.technopark</a></p>
<p><a href="https://kediritechnopark.com" target="_blank" rel="noopener noreferrer">www.KEDIRITECHNOPARK.com</a></p>
</Col>
<Col lg={6} className="text-center">
<Col lg={6}>
<div className="footer-widget">
<h4>About Our Company</h4>
<div className="logo mb-3">
<img src="/assets/images/logo-white.png" alt="Logo" className="img-fluid" />
<div className={styles.logo}>
<img src="https://kediritechnopark.com/kediri-technopark-logo-white.png" alt="Logo" />
</div>
<p>Kediri Technopark adalah pusat pengembangan inovasi digital dan aplikasi untuk masyarakat dan pelaku usaha.</p>
</div>
</Col>
<Col lg={12} className="text-center mt-3">
<Col lg={12} className="mt-3">
<div className="copyright-text">
<p>
&copy; 2025 Kediri Technopark. All Rights Reserved.<br />
Design by <a href="https://templatemo.com/" target="_blank" rel="noopener noreferrer" className="text-white">TemplateMo</a><br />
Distributed by <a href="https://themewagon.com/" target="_blank" rel="noopener noreferrer" className="text-white">ThemeWagon</a>
</p>
<p>&copy; 2025 Kediri Technopark. All Rights Reserved.</p>
</div>
</Col>
</Row>

View File

@@ -26,12 +26,23 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
onMouseEnter={() => setHoveredNav(3)}
onMouseLeave={() => setHoveredNav(null)}
onClick={() => {
if (!username) scrollToCourse();
if (!username) scrollToProduct();
else navigate('/products');
}}
>
{username ? 'MY PRODUCTS' : 'PRODUCTS'}
</a>
<a
className={`${styles.navLink} ${hoveredNav === 4 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(4)}
onMouseLeave={() => setHoveredNav(null)}
onClick={() => {
if (!username) scrollToCourse();
else window.location.href = 'https://academy.kediritechnopark.com'
}}
>
{username ? 'MY ACADEMY' : 'ACADEMY'}
</a>
</nav>
{/* Burger Menu Button */}

View File

@@ -1,21 +1,36 @@
// HeroSection.jsx — 2025 refresh using React-Bootstrap + CSS Module
import React from 'react';
import { Container, Row, Col, Button } from 'react-bootstrap';
import styles from './HeroSection.module.css';
const HeroSection = () => {
return (
<section className="hero-section pt-5 bg-light">
<section className={`${styles.hero} pt-5`}
aria-label="Kediri Technopark hero section">
<Container>
<Row className="align-items-center">
<Col lg={6}>
<h1>KATALIS KARIR DAN BISNIS DIGITAL</h1>
<p>Kami adalah ekosistem tempat mimpi digital tumbuh dan masa depan dibentuk. Di sinilah semangat belajar bertemu dengan inovasi, dan ide-ide muda diberi ruang untuk berkembang. Lebih dari sekadar tempat, kami adalah rumah bagi talenta, teknologi, dan transformasi. Mari jelajahi dunia digital, bangun karir, dan ciptakan solusi semua dimulai dari sini.</p>
<div className="d-flex gap-3">
<Button variant="outline-primary" href="https://instagram.com/kediri.technopark" target="_blank">Instagram</Button>
<Button variant="outline-success" href="tel:+6281318894994">WhatsApp</Button>
<Row className="align-items-center gy-5">
{/* Image on top for mobile, text first on lg+ */}
<Col xs={{ order: 1 }} lg={{ span: 6, order: 1 }}>
<div className={styles.copyWrap}>
<h1 className={styles.title}>
KATALIS KARIR DAN BISNIS DIGITAL
</h1>
<p className={styles.lead}>
Kami adalah ekosistem tempat mimpi digital tumbuh dan masa depan dibentuk. Di sinilah semangat belajar bertemu dengan inovasi, dan ide-ide muda diberi ruang untuk berkembang. Lebih dari sekadar tempat, kami adalah rumah bagi talenta, teknologi, dan transformasi. Mari jelajahi dunia digital, bangun karir, dan ciptakan solusi semua dimulai dari sini.
</p>
</div>
</Col>
<Col lg={6}>
<img src="https://kediritechnopark.com/assets/images/gambar1.png" alt="Hero Image" className="img-fluid" />
<Col xs={{ order: 0 }} lg={{ span: 6, order: 2 }}>
<div className={styles.imageWrap}>
<img
src="https://kediritechnopark.com/assets/hero.png"
alt="Ekosistem digital Kediri Technopark"
className={`img-fluid ${styles.heroImage}`}
loading="lazy"
decoding="async"
/>
<div className={styles.glow} aria-hidden="true" />
</div>
</Col>
</Row>
</Container>

View File

@@ -0,0 +1,109 @@
.hero {
position: relative;
background: radial-gradient(1200px 600px at 10% -10%, rgba(37, 99, 235, 0.15), transparent 60%),
radial-gradient(1000px 500px at 110% 10%, rgba(34, 197, 94, 0.15), transparent 60%),
var(--surface);
overflow: clip;
}
.copyWrap {
max-width: var(--hero-maxw);
}
.title {
font-weight: 800;
line-height: 1.08;
letter-spacing: -0.02em;
/* Fluid type: min 28px → max 56px */
font-size: clamp(1.75rem, 2.5vw + 1rem, 3.5rem);
background: linear-gradient(92deg, var(--text), #3b82f6 40%, #22c55e 90%);
-webkit-background-clip: text;
background-clip: text;
color: black;
}
.lead {
margin-top: 1rem;
color: var(--muted);
font-size: clamp(1rem, 0.6vw + 0.95rem, 1.2rem);
}
.ctaGroup {
margin-top: 1.25rem;
}
.cta {
--ring: 0 0 0 0 rgba(37,99,235,0);
border-radius: var(--radius-2xl) !important;
padding: 0.625rem 1rem !important;
font-weight: 600 !important;
backdrop-filter: saturate(140%);
transition: transform .2s ease, box-shadow .2s ease, background-color .2s ease;
box-shadow: var(--shadow-soft);
}
.ctaPrimary:hover,
.ctaPrimary:focus-visible {
transform: translateY(-1px);
box-shadow: 0 12px 32px rgba(37, 99, 235, 0.25);
}
.ctaSecondary:hover,
.ctaSecondary:focus-visible {
transform: translateY(-1px);
box-shadow: 0 12px 32px rgba(34, 197, 94, 0.25);
}
.imageWrap {
position: relative;
display: grid;
place-items: center;
isolation: isolate;
}
.imageWrap::before,
.imageWrap::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
width: 80px; /* lebar gradasi di sisi */
pointer-events: none; /* biar nggak ganggu klik */
z-index: 2;
}
.imageWrap::before {
left: 0;
background: linear-gradient(to right, rgba(255,255,255,1), rgba(255,255,255,0));
}
.imageWrap::after {
right: 0;
background: linear-gradient(to left, rgba(255,255,255,1), rgba(255,255,255,0));
}
.heroImage {
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-soft);
}
.glow {
position: absolute;
inset: auto 10% -10% 10%;
height: 40%;
filter: blur(40px);
z-index: -1;
background: radial-gradient(60% 60% at 50% 0%, rgba(59,130,246,.35), transparent 60%),
radial-gradient(50% 50% at 30% 60%, rgba(34,197,94,.25), transparent 60%);
}
/* Fine-tuned responsive spacing */
@media (min-width: 992px) {
.copyWrap { padding-right: 1rem; }
}
@media (max-width: 575.98px) {
.hero { padding-top: 2rem; }
}

View File

@@ -6,10 +6,10 @@ const KnowledgeBaseSection = () => {
<section id="knowledge" className="knowledge section pt-5">
<Container>
<Row>
<Col lg={{ span: 8, offset: 2 }}>
<div className="section-heading text-center mb-4">
<Col >
<div className="section-heading mb-4">
<h4>KNOWLEDGE <em>BASE</em></h4>
<img src="/assets/images/heading-line-dec.png" alt="" className="mb-3" />
{/* <img src="/assets/images/heading-line-dec.png" alt="" className="mb-3" /> */}
<p>Berbagai artikel dan panduan untuk membantu Anda memahami teknologi dan inovasi digital.</p>
</div>
<div className="knowledge-content">

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
const LoginRegister = ({setShowedModal}) => {
const [tab, setTab] = useState('login'); // 'login' or 'register'
const [email, setEmail] = useState('');
const [username, setUsername] = useState('');
@@ -11,8 +11,7 @@ const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
backgroundColor: 'white',
borderRadius: '1rem',
padding: '2rem',
maxWidth: '400px',
margin: '0 auto',
width: '100%',
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)',
fontFamily: 'Inter, system-ui, sans-serif',
},
@@ -78,7 +77,7 @@ const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
}
try {
const res = await fetch('https://bot.kediritechnopark.com/webhook/user-dev/login', {
const res = await fetch('https://bot.kediritechnopark.com/webhook/user-production/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
@@ -95,11 +94,17 @@ const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
if (token) {
document.cookie = `token=${token}; path=/; max-age=${7 * 24 * 60 * 60}`;
if (postLoginAction) {
postLoginAction(); // resume action (e.g., checkout)
setPostLoginAction(null);
const params = new URLSearchParams(window.location.search);
const nextAction = params.get('next');
if (nextAction === 'checkout') {
params.delete('next');
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.replaceState({}, '', newUrl);
setShowedModal('product');
} else {
window.location.reload();
}
// window.location.reload()
} else {
alert('Token tidak ditemukan pada respons login');
}
@@ -117,7 +122,7 @@ const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
}
try {
const res = await fetch('https://bot.kediritechnopark.com/webhook/user-dev/register', {
const res = await fetch('https://bot.kediritechnopark.com/webhook/user-production/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, username, password }),

View File

@@ -10,7 +10,7 @@
}
.image {
width: 40vw;
width: 100%;
height: 260px;
background-color: #e2e8f0;
border-radius: 0.75rem;
@@ -22,17 +22,17 @@
margin-bottom: 1.5rem;
background-repeat: no-repeat;
background-size: cover;
background-size: contain;
background-position: center;
}
.headerRow {
display: flex;
justify-content: center;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 1rem;
gap: 0.5rem; /* optional: supaya nggak terlalu rapat saat wrap */
margin-bottom: 1rem;
text-align: center;
}
.title {
@@ -53,6 +53,7 @@
color: #64748b;
margin-bottom: 1.5rem;
line-height: 1.6;
text-align: left;
}
.buttonGroup {
@@ -90,16 +91,9 @@
.container {
text-align: left;
}
.headerRow {
justify-content: flex-start;
}
.buttonGroup {
gap: 0.5rem;
}
.image {
width: 63vw;
}
}
.childSelector {

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import styles from './ProductDetail.module.css';
const ProductDetail = ({ subscriptions, product, setPostLoginAction, setShowedModal }) => {
const ProductDetail = ({ subscriptions, product, requestLogin, setShowedModal }) => {
const [showChildSelector, setShowChildSelector] = useState(false);
const [selectedChildIds, setSelectedChildIds] = useState([]);
@@ -12,44 +12,27 @@ const ProductDetail = ({ subscriptions, product, setPostLoginAction, setShowedMo
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;
}
};
const onCheckout = () => {
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
if (!token) {
setPostLoginAction(() => () => setShowedModal('product'));
setShowedModal('login');
requestLogin('checkout');
return;
}
if (product.type == 'product') {
const hasMatchingSubscription = Array.isArray(subscriptions) &&
subscriptions.some(sub =>
sub.product_id === product.id || sub.product_parent_id === product.id
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
);
// Always show children selector first if product has children
if (product.children && product.children.length > 0) {
// Always show children selector first if product has children
if (product.children && product.children.length > 0) {
setShowChildSelector(true);
if (hasMatchingSubscription) {
const matching = subscriptions.filter(sub =>
sub.product_id === product.id || sub.product_parent_id === product.id
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
);
if (matching.length > 0) {
@@ -62,15 +45,15 @@ if (product.children && product.children.length > 0) {
}
}
return;
}
}
// No children, but has subscription match
if (hasMatchingSubscription) {
// No children, but has subscription match
if (hasMatchingSubscription) {
const matching = subscriptions.filter(sub =>
sub.product_id === product.id || sub.product_parent_id === product.id
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
);
if (matching.length > 0) {
if (matching.length > 0 && !product.end_date) {
const uniqueByName = Array.from(
new Map(matching.map(sub => [sub.product_name, sub])).values()
);
@@ -79,13 +62,20 @@ if (hasMatchingSubscription) {
setShowSubscriptionSelector(true);
return;
}
}
else {
const itemsParam = JSON.stringify([product.id]);
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${product.name}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
return;
}
}
setShowNamingInput(true);
return;
}
// 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`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
};
const onConfirmChildren = () => {
@@ -94,6 +84,11 @@ if (hasMatchingSubscription) {
setShowSubscriptionSelector(true);
return;
}
else {
setShowChildSelector(false);
setShowNamingInput(true);
return;
}
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
@@ -104,7 +99,7 @@ if (hasMatchingSubscription) {
}
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`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
};
const onFinalCheckoutNewProduct = () => {
@@ -118,7 +113,7 @@ if (hasMatchingSubscription) {
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`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
};
const onConfirmSelector = () => {
@@ -127,7 +122,7 @@ if (hasMatchingSubscription) {
return;
}
if (selectedSubscriptionId === product.id) {
if (selectedSubscriptionId === 0) {
setShowNamingInput(true);
} else {
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
@@ -140,28 +135,47 @@ if (hasMatchingSubscription) {
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`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${encodedName}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
}
};
const priceColor = product.price === 0 ? '#059669' : '#2563eb';
console.log(product)
return (
<div className={styles.container}>
{!showChildSelector && !showSubscriptionSelector && !showNamingInput && (
<>
<div className={styles.image} style={{ backgroundImage: `url(${product.image})` }}></div>
<div className={styles.headerRow}>
<h2 className={styles.title}>{product.name}</h2>
<h2 className={styles.title}>{product.name.split('%%%')[0]}</h2>
<div className={styles.price} style={{ color: priceColor }}>
{product.price == null ? 'Pay-As-You-Go' : `Rp ${parseInt(product.price).toLocaleString('id-ID')}`}
</div>
</div>
<p className={styles.description}>{product.description}</p>
<div className={styles.buttonGroup}>
<button className={`${styles.button} ${styles.checkoutButton}`} onClick={onCheckout}>
Checkout
{(product.site_url || product.end_date || product.quantity) && (
<button
className={`${styles.button} ${styles.checkoutButton}`}
onClick={() => {
const token = (document.cookie.split('; ').find(row => row.startsWith('token=')) || '').split('=')[1] || '';
const url = product.quantity || product.end_date
? `https://${product.site_url}/${product.name.split('%%%')[0]}?token=${token}`
: `https://${product.site_url}`;
window.location.href = url;
}}
>
{product.end_date || product.quantity ? 'KUNJUNGI SITUS' : 'PELAJARI LEBIH LANJUT'}
</button>
)}
<button className={`${styles.button} ${styles.checkoutButton}`} onClick={onCheckout}>
{Array.isArray(subscriptions) &&
subscriptions.some(sub =>
sub.product_id === product.id || sub.product_parent_id === product.id
) && product.end_date ? 'Perpanjang' : 'Checkout'}
</button>
</div>
</>
)}
@@ -204,7 +218,7 @@ if (hasMatchingSubscription) {
{showSubscriptionSelector && !showNamingInput && (
<div className={styles.childSelector}>
<h5>Perpanjang {product.name}</h5>
<h5>Perpanjang {product.name.split('%%%')[0]} </h5>
{matchingSubscriptions.map(sub => (
<label key={sub.id} className={styles.childProduct}>
<input
@@ -214,7 +228,7 @@ if (hasMatchingSubscription) {
checked={selectedSubscriptionId == sub.id}
onChange={() => { setSelectedSubscriptionId(sub.id); setCustomName(sub.product_name) }}
/>
&nbsp;{sub.product_name}
&nbsp;{sub.product_name.split('%%%')[0]}
</label>
))}
<h6>Atau buat baru</h6>
@@ -222,10 +236,10 @@ if (hasMatchingSubscription) {
<input
type="radio"
name="subscription"
checked={selectedSubscriptionId === product.id}
onChange={() => setSelectedSubscriptionId(product.id)}
checked={selectedSubscriptionId === 0}
onChange={() => {setSelectedSubscriptionId(0); console.log(product.id)}}
/>
&nbsp;Buat {product.name} baru
&nbsp;Buat {product.name.split('%%%')[0]} baru
</label>
<div className={styles.buttonGroup}>
<button className={styles.button} onClick={() => setShowSubscriptionSelector(false)}>
@@ -238,9 +252,9 @@ if (hasMatchingSubscription) {
</div>
)}
{showNamingInput && (
{showNamingInput && (
<div className={styles.childSelector}>
<h5>Buat {product.name} Baru</h5>
<h5>Buat {product.name.split('%%%')[0]} Baru</h5>
<input
type="text"
placeholder="Nama produk..."
@@ -265,7 +279,12 @@ if (hasMatchingSubscription) {
className={styles.button}
onClick={() => {
setShowNamingInput(false);
setShowSubscriptionSelector(true);
const hasMatchingSubscription = Array.isArray(subscriptions) &&
subscriptions.some(sub =>
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
);
if (hasMatchingSubscription) setShowSubscriptionSelector(true);
}}
>
Kembali
@@ -284,7 +303,7 @@ if (hasMatchingSubscription) {
</button>
</div>
</div>
)}
)}
</div>
);

View File

@@ -1,13 +1,17 @@
import React, { useEffect, useState } from 'react';
import { Container } from 'react-bootstrap';
import styles from './Styles.module.css';
import processProducts from '../helper/processProducts';
const ProductSection = ({ hoveredCard, setHoveredCard, setSelectedProduct, setShowedModal, productSectionRef }) => {
const [products, setProducts] = useState([]);
// Define this function outside useEffect so it can be called anywhere
// Inside your component
useEffect(() => {
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -16,34 +20,7 @@ useEffect(() => {
})
.then(res => res.json())
.then(data => {
const parentMap = {};
const childrenMap = {};
// Pisahkan parent dan child
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,
children: []
};
}
});
// Pasang children ke parent
Object.keys(childrenMap).forEach(parentId => {
const parent = parentMap[parentId];
if (parent) {
parent.children = childrenMap[parentId];
}
});
// Ambil parent saja
const enrichedData = Object.values(parentMap);
const enrichedData = processProducts(data);
setProducts(enrichedData);
})
.catch(err => console.error('Fetch error:', err));
@@ -51,9 +28,9 @@ useEffect(() => {
return (
<section id="services" className="services pt-5">
<section id="services" className="services pt-5" ref={productSectionRef}>
<Container>
<div className="section-heading text-center mb-4">
<div className="section-heading mb-4">
<h4>OUR <em>PRODUCTS</em></h4>
<img src="/assets/images/heading-line-dec.png" alt="" />
<p>Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.</p>
@@ -73,14 +50,18 @@ useEffect(() => {
onMouseEnter={() => setHoveredCard(product.id)}
onMouseLeave={() => setHoveredCard(null)}
>
<div>
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
{product.price === 0 && (
<span className={styles.courseLabel}>Free</span>
)}
</div>
<div className={styles.courseContent}>
<div className={styles.courseContentTop}>
<h3 className={styles.courseTitle}>{product.name}</h3>
<p className={styles.courseDesc}>{product.description}</p>
</div>
</div>
<div className={styles.courseContentBottom}>
<div className={styles.coursePrice}>
<span
className={

View File

@@ -5,7 +5,7 @@ const ServicesSection = () => {
return (
<section id="services" className="services py-5">
<Container>
<div className="section-heading text-center mb-4">
<div className="section-heading mb-4">
<h4>OUR <em>SERVICES</em></h4>
<img src="/assets/images/heading-line-dec.png" alt="" />
<p>Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.</p>

View File

@@ -29,7 +29,7 @@
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: sticky;
/* position: sticky; */
top: 0;
z-index: 1000;
}
@@ -194,14 +194,14 @@
box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
cursor: pointer;
/* Tambahan untuk menghilangkan highlight biru */
-webkit-tap-highlight-color: transparent;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline: none;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.courseCard:focus {
@@ -217,7 +217,7 @@
.courseImage {
width: 100%;
height: 200px;
background-color: #e2e8f0;
background-color: white;
position: relative;
display: flex;
align-items: center;
@@ -225,7 +225,7 @@
color: #64748b;
background-repeat: no-repeat;
background-size: cover;
background-size: contain;
background-position: center;
}
@@ -241,8 +241,20 @@
font-weight: 600;
}
.courseContent {
padding: 1.5rem;
.courseContentTop {
padding: 1rem 1rem 0rem;
text-align: left;
}
.courseContentTop p {
margin-top: 0.5rem;
margin-bottom: 1rem;
}
.courseContentBottom {
padding: 1rem;
padding-top: 0;
text-align: left;
}
@@ -490,13 +502,11 @@
.ctaTitle,
.courseTitle {
font-size: 15px;
margin: 0;
}
.ctaDescription,
.courseContent p {
font-size: 13px;
.courseContentTop, .courseContentBottom p {
margin: 6px 0px;
}
@@ -506,9 +516,8 @@
background-color: #f8fafc;
}
.courseContent {
.courseContentTop, .courseContentBottom {
text-align: left;
padding: 0.8rem;
}
.ctaContainer,
@@ -627,3 +636,35 @@
background-color: #2563eb;
color: white;
}
.clientLogoWrapper {
max-width: 150px; /* batas lebar */
max-height: 80px; /* batas tinggi */
display: flex;
align-items: center;
justify-content: center;
}
.clientLogo {
max-height: 80px;
width: auto;
object-fit: contain;
}
.footer{
padding: 0;
}
.footer a {
color: white;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline; /* opsional kalau mau hover effect */
}
.logo img {
max-width: 150px; /* biar logo tidak terlalu besar */
height: auto;
}

View File

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

View File

@@ -0,0 +1,30 @@
// processProducts.js
export default function processProducts(data) {
const parentMap = {};
const childrenMap = {};
// Pisahkan parent dan child
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,
children: []
};
}
});
// Pasang children ke parent
Object.keys(childrenMap).forEach(parentId => {
const parent = parentMap[parentId];
if (parent) {
parent.children = childrenMap[parentId];
}
});
// Ambil parent saja
return Object.values(parentMap);
}