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 { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import styles from './components/Styles.module.css'; import styles from './components/Styles.module.css';
// Import components
import Login from './components/Login'; import Login from './components/Login';
import Header from './components/Header'; import Header from './components/Header';
import HeroSection from './components/HeroSection'; import HeroSection from './components/HeroSection';
import ServicesSection from './components/ServicesSection'; import ServicesSection from './components/ServicesSection';
@@ -14,13 +12,12 @@ import AboutUsSection from './components/AboutUsSection';
import KnowledgeBaseSection from './components/KnowledgeBaseSection'; import KnowledgeBaseSection from './components/KnowledgeBaseSection';
import ClientsSection from './components/ClientsSection'; import ClientsSection from './components/ClientsSection';
import Footer from './components/Footer'; import Footer from './components/Footer';
import ProductDetailPage from './components/ProductDetailPage'; import ProductDetailPage from './components/ProductDetailPage';
import Dashboard from './components/Dashboard'; import Dashboard from './components/Dashboard';
import CreateProductPage from './components/CreateProductPage';
import ProductsPage from './components/pages/ProductsPage'; import ProductsPage from './components/pages/ProductsPage';
import processProducts from './helper/processProducts';
function HomePage({ function HomePage({
hoveredCard, hoveredCard,
@@ -32,10 +29,11 @@ function HomePage({
productSectionRef, productSectionRef,
courseSectionRef courseSectionRef
}) { }) {
return ( return (
<> <>
<HeroSection /> <HeroSection />
<AboutUsSection />
<ServicesSection /> <ServicesSection />
<ProductSection <ProductSection
productSectionRef={productSectionRef} productSectionRef={productSectionRef}
@@ -49,13 +47,14 @@ function HomePage({
hoveredCard={hoveredCard} hoveredCard={hoveredCard}
setHoveredCard={setHoveredCard} setHoveredCard={setHoveredCard}
setSelectedProduct={setSelectedProduct} setSelectedProduct={setSelectedProduct}
setShowedModal={setShowedModal} /> setShowedModal={setShowedModal}
<AboutUsSection /> />
<KnowledgeBaseSection /> <KnowledgeBaseSection />
<ClientsSection /> <ClientsSection />
</> </>
); );
} }
function parseJwt(token) { function parseJwt(token) {
try { try {
const base64Url = token.split('.')[1]; const base64Url = token.split('.')[1];
@@ -74,60 +73,188 @@ function parseJwt(token) {
function App() { function App() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// State yang diperlukan untuk HomePage
const [hoveredCard, setHoveredCard] = useState(null); const [hoveredCard, setHoveredCard] = useState(null);
const [subscriptions, setSubscriptions] = useState(null); const [subscriptions, setSubscriptions] = useState(null);
const [selectedProduct, setSelectedProduct] = useState({}); const [selectedProduct, setSelectedProduct] = useState({});
const [showedModal, setShowedModal] = useState(null); // 'product' | 'login' | null const [showedModal, setShowedModal] = useState(null);
const [postLoginAction, setPostLoginAction] = useState(null); const [subProductOf, setSubProductOf] = useState(null);
const [username, setUsername] = useState(null); const [username, setUsername] = useState(null);
const productSectionRef = useRef(null); const productSectionRef = useRef(null);
const courseSectionRef = 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(() => { useEffect(() => {
// Ambil token dari cookies
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)')); const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
if (match) { if (match) {
const token = match[2]; const token = match[2];
fetch('https://bot.kediritechnopark.com/webhook/user-production/data', {
fetch('https://bot.kediritechnopark.com/webhook/user-dev/data', {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token 'Authorization': 'Bearer ' + token
}, },
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (data && data.token) { if (data && data.token) {
// Update token with data[0].token
document.cookie = `token=${data.token}; path=/`; document.cookie = `token=${data.token}; path=/`;
console.log(data) setSubscriptions(data.subscriptions);
setSubscriptions(data.subscriptions)
const payload = parseJwt(data.token); const payload = parseJwt(data.token);
if (payload && payload.username) { 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)); .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" }); useEffect(() => {
}; const params = new URLSearchParams(window.location.search);
const scrollToCourse = () => { const modalType = params.get('modal');
courseSectionRef.current?.scrollIntoView({ behavior: "smooth" }); 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(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@@ -137,13 +264,8 @@ function App() {
}, []); }, []);
const handleLogout = () => { 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'; document.cookie = 'token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
setUsername(null);
// Jika kamu menggunakan state seperti `setUsername`, bersihkan di sini juga
setUsername(null); // jika applicable
// Redirect ke homepage atau reload halaman
window.location.reload(); window.location.reload();
}; };
@@ -165,7 +287,13 @@ function App() {
return ( return (
<Router> <Router>
<div className="App"> <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> <Routes>
<Route <Route
path="/" path="/"
@@ -182,47 +310,57 @@ function App() {
/> />
} }
/> />
<Route <Route path="/products" element={<ProductsPage subscriptions={subscriptions} />} />
path="/products"
element={
<ProductsPage subscriptions={subscriptions}/>
}
/>
<Route <Route
path="/dashboard" path="/dashboard"
element={ element={
<Dashboard /> <Dashboard
setShowedModal={(e, productId) => {
setShowedModal(e);
setSubProductOf(productId);
}}
/>
} }
/> />
</Routes> </Routes>
<Footer /> <Footer />
{/* Unified Modal */}
{/* Modal */}
{showedModal && ( {showedModal && (
<div <div
className={styles.modal} className={styles.modal}
onClick={() => { 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); setShowedModal(null);
setSelectedProduct({}); setSelectedProduct({});
}} }}
> >
<div <div className={styles.modalBody} onClick={(e) => e.stopPropagation()}>
className={styles.modalBody}
onClick={(e) => e.stopPropagation()}
>
{showedModal === 'product' && ( {showedModal === 'product' && (
<ProductDetailPage <ProductDetailPage
subscriptions={subscriptions} subscriptions={subscriptions}
setPostLoginAction={setPostLoginAction} requestLogin={requestLogin}
setShowedModal={setShowedModal}
product={selectedProduct} product={selectedProduct}
onClose={() => { setShowedModal={setShowedModal}
setShowedModal(null); />
setSelectedProduct({}); )}
}} {showedModal === 'create-item' && (
<CreateProductPage
parentId={subProductOf}
subscriptions={subscriptions}
requestLogin={requestLogin}
product={selectedProduct}
setShowedModal={setShowedModal}
/> />
)} )}
{showedModal === 'login' && ( {showedModal === 'login' && (
<Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} /> <Login setShowedModal={setShowedModal} />
)} )}
</div> </div>
</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. 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> </p>
<div className="mt-4 d-flex gap-3"> <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' }}> <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' }}>
Konsultasi Instagram
</Button> </Button>
<Button href="https://wa.me/6281318894994" target="_blank" variant="outline-success" className="px-4 py-2 rounded-pill"> <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 <i className="fab fa-whatsapp"></i> WhatsApp

View File

@@ -6,7 +6,7 @@ const AcademySection = ({hoveredCard, setHoveredCard, setSelectedProduct, setSho
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
useEffect(() => { useEffect(() => {
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', { fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -20,17 +20,19 @@ const AcademySection = ({hoveredCard, setHoveredCard, setSelectedProduct, setSho
return ( return (
<section id="services" className="services pt-5"> <section id="services" className="services pt-5" ref={courseSectionRef}>
<Container> <Container>
<div className="section-heading text-center mb-4"> <div className="section-heading mb-4">
<h4>OUR <em>ACADEMY PROGRAM</em></h4> <h4>OUR <em>ACADEMY PROGRAM</em></h4>
<img src="/assets/images/heading-line-dec.png" alt="" /> <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> <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>
<div className={styles.coursesGrid}> <div className={styles.coursesGrid}>
{products && {products &&
products[0]?.name && products[0]?.name &&
products.map(product => ( products
.map(product => (
<div <div
key={product.id} key={product.id}
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`} className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
@@ -41,14 +43,18 @@ const AcademySection = ({hoveredCard, setHoveredCard, setSelectedProduct, setSho
onMouseEnter={() => setHoveredCard(product.id)} onMouseEnter={() => setHoveredCard(product.id)}
onMouseLeave={() => setHoveredCard(null)} onMouseLeave={() => setHoveredCard(null)}
> >
<div>
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}> <div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
{product.price === 0 && ( {product.price === 0 && (
<span className={styles.courseLabel}>Free</span> <span className={styles.courseLabel}>Free</span>
)} )}
</div> </div>
<div className={styles.courseContent}> <div className={styles.courseContentTop}>
<h3 className={styles.courseTitle}>{product.name}</h3> <h3 className={styles.courseTitle}>{product.name}</h3>
<p className={styles.courseDesc}>{product.description}</p> <p className={styles.courseDesc}>{product.description}</p>
</div>
</div>
<div className={styles.courseContentBottom}>
<div className={styles.coursePrice}> <div className={styles.coursePrice}>
<span <span
className={ className={

View File

@@ -1,23 +1,35 @@
import React from 'react'; import React from 'react';
import { Container, Row, Col, Image } from 'react-bootstrap'; import { Container, Row, Col, Image } from 'react-bootstrap';
import styles from './Styles.module.css';
const ClientsSection = () => { const ClientsSection = () => {
const logos = [
'dermalounge.jpg',
'suar.avif',
'kloowear.png',
'psi.png',
];
return ( return (
<section id="clients" className="the-clients section pt-5"> <section id="clients" className="the-clients section py-5">
<Container> <Container>
<Row> <Row>
<Col lg={{ span: 8, offset: 2 }}> <Col>
<div className="section-heading text-center mb-4"> <div className="section-heading mb-4">
<h4>TRUSTED BY <em>OUR CLIENTS</em></h4> <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> <p>We are proud to work with these amazing brands and organizations.</p>
</div> </div>
<div id="clients-carousel" className="d-flex justify-content-center flex-wrap"> <div id="clients-carousel" className="d-flex justify-content-left flex-wrap">
{[1, 2, 3, 4, 5].map((num) => ( {logos.map((logo, index) => (
<div key={num} className="client-logo-wrapper m-2"> <div className={`${styles.clientLogoWrapper} m-2`} key={index}>
<Image src={`/assets/images/client-logo${num}.png`} alt={`Client ${num}`} fluid /> <Image
src={`https://kediritechnopark.com/assets/${logo}`}
fluid
className={styles.clientLogo}
/>
</div>
))}
</div> </div>
))}</div>
</Col> </Col>
</Row> </Row>
</Container> </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 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 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({ const [dashboardData, setDashboardData] = useState({
totalRevenue: { totalRevenue: { amount: 10215845, currency: 'IDR', change: 33.87, period: '22 - 29 May 2025' },
amount: 10215845, totalItemsSold: { amount: 128980, change: -33.87, period: '22 - 29 May 2025' },
currency: 'IDR', totalVisitors: { amount: 2905897, change: 33.87, period: '22 - 29 May 2025' },
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: [ chartData: [
{ date: '22/06', items: 200, revenue: 800 }, { date: '22/06', items: 200, revenue: 800 },
{ date: '23/06', items: 750, revenue: 450 }, { date: '23/06', items: 750, revenue: 450 },
{ date: '24/06', items: 550, revenue: 200 }, { date: '24/06', items: 550, revenue: 200 },
{ date: '24/06', items: 300, revenue: 350 }, { date: '25/06', items: 300, revenue: 350 },
{ date: '24/06', items: 900, revenue: 450 }, { date: '26/06', items: 900, revenue: 450 },
{ date: '24/06', items: 550, revenue: 200 }, { date: '27/06', items: 550, revenue: 200 },
{ date: '24/06', items: 700, revenue: 300 }
], ],
latestTransactions: [ latestTransactions: []
{
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 useEffect(() => {
const connectToN8NWebhook = async (webhookUrl) => { const fetchDistinctOptions = async () => {
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
if (!match) return;
const token = match[2];
try { try {
const response = await fetch(webhookUrl, { const res = await fetch('https://bot.kediritechnopark.com/webhook/store-production/get-products', {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}, },
}); });
if (response.ok) { const result = await res.json();
const data = await response.json(); const productsArr = result || [];
setDashboardData(data);
} const types = [...new Set(productsArr.map(p => p.type).filter(Boolean))];
} catch (error) { const groups = [...new Set(productsArr.map(p => p.group).filter(Boolean))];
console.error('Error connecting to n8n webhook:', error);
setAvailableTypes(types);
setAvailableGroups(groups);
setProducts(processProducts(productsArr));
} catch (err) {
console.error('Gagal ambil produk:', err);
} }
}; };
// Function untuk send data ke n8n webhook fetchDistinctOptions();
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) { const formatCurrency = (amount) => new Intl.NumberFormat('id-ID').format(amount);
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) => { const getStatusClass = (status) => {
switch (status) { switch (status) {
case 'confirmed': case 'confirmed': return styles.statusConfirmed;
return styles.statusConfirmed; case 'waiting payment': return styles.statusWaiting;
case 'waiting payment': case 'payment expired': return styles.statusExpired;
return styles.statusWaiting; default: return styles.statusConfirmed;
case 'payment expired':
return styles.statusExpired;
default:
return styles.statusConfirmed;
} }
}; };
@@ -134,11 +81,9 @@ const Dashboard = () => {
<h3 className={styles.statCardTitle}>{title}</h3> <h3 className={styles.statCardTitle}>{title}</h3>
<Icon className={styles.statCardIcon} /> <Icon className={styles.statCardIcon} />
</div> </div>
<div className={styles.statCardValue}> <div className={styles.statCardValue}>
{currency && `${currency} `}{formatCurrency(value)} {currency && `${currency} `}{formatCurrency(value)}
</div> </div>
<div className={styles.statCardFooter}> <div className={styles.statCardFooter}>
<div className={styles.statCardChange}> <div className={styles.statCardChange}>
{isNegative ? ( {isNegative ? (
@@ -152,31 +97,19 @@ const Dashboard = () => {
<span className={styles.fromLastWeek}>from last week</span> <span className={styles.fromLastWeek}>from last week</span>
</div> </div>
</div> </div>
<div className={styles.statCardPeriod}>{period}</div> <div className={styles.statCardPeriod}>{period}</div>
</div> </div>
); );
const BarChart = ({ data }) => { const BarChart = ({ data }) => {
const maxValue = Math.max(...data.map(item => Math.max(item.items, item.revenue))); const maxValue = Math.max(...data.map(item => Math.max(item.items, item.revenue)));
return ( return (
<div className={styles.barChart}> <div className={styles.barChart}>
{data.map((item, index) => ( {data.map((item, index) => (
<div key={index} className={styles.barGroup}> <div key={index} className={styles.barGroup}>
<div className={styles.barContainer}> <div className={styles.barContainer}>
<div <div className={`${styles.bar} ${styles.barItems}`} style={{ height: `${(item.items / maxValue) * 200}px` }} />
className={`${styles.bar} ${styles.barItems}`} <div className={`${styles.bar} ${styles.barRevenue}`} style={{ height: `${(item.revenue / maxValue) * 200}px` }} />
style={{
height: `${(item.items / maxValue) * 200}px`
}}
/>
<div
className={`${styles.bar} ${styles.barRevenue}`}
style={{
height: `${(item.revenue / maxValue) * 200}px`
}}
/>
</div> </div>
<span className={styles.barLabel}>{item.date}</span> <span className={styles.barLabel}>{item.date}</span>
</div> </div>
@@ -187,92 +120,70 @@ const Dashboard = () => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
{/* Stats Cards */}
<div className={styles.statsGrid}> <div className={styles.statsGrid}>
<StatCard <StatCard title="Total Revenue" value={dashboardData.totalRevenue.amount} currency="IDR" change={dashboardData.totalRevenue.change} period={dashboardData.totalRevenue.period} icon={DollarSign} isNegative={false} />
title="Total Revenue" <StatCard title="Total Items Sold" value={dashboardData.totalItemsSold.amount} change={dashboardData.totalItemsSold.change} period={dashboardData.totalItemsSold.period} icon={ShoppingCart} isNegative={true} />
value={dashboardData.totalRevenue.amount} <StatCard title="Total Visitor" value={dashboardData.totalVisitors.amount} change={dashboardData.totalVisitors.change} period={dashboardData.totalVisitors.period} icon={Users} isNegative={false} />
currency={dashboardData.totalRevenue.currency}
change={dashboardData.totalRevenue.change}
period={dashboardData.totalRevenue.period}
icon={DollarSign}
isNegative={false}
/>
<StatCard
title="Total Items Sold"
value={dashboardData.totalItemsSold.amount}
change={dashboardData.totalItemsSold.change}
period={dashboardData.totalItemsSold.period}
icon={ShoppingCart}
isNegative={true}
/>
<StatCard
title="Total Visitor"
value={dashboardData.totalVisitors.amount}
change={dashboardData.totalVisitors.change}
period={dashboardData.totalVisitors.period}
icon={Users}
isNegative={false}
/>
</div> </div>
{/* Charts and Transactions */}
<div className={styles.chartsGrid}> <div className={styles.chartsGrid}>
{/* Report Statistics */} {/* Tempatkan <BarChart data={dashboardData.chartData} /> jika mau ditampilkan */}
<div className={styles.chartCard}>
<div className={styles.chartHeader}>
<div>
<h3 className={styles.chartTitle}>Report Statistics</h3>
<p className={styles.chartSubtitle}>Period: 22 - 29 May 2025</p>
</div>
<div className={styles.chartLegend}>
<div className={styles.legendItem}>
<div className={`${styles.legendColor} ${styles.legendColorGreen}`}></div>
<span className={styles.legendText}>Items Sold</span>
</div>
<div className={styles.legendItem}>
<div className={`${styles.legendColor} ${styles.legendColorLightGreen}`}></div>
<span className={styles.legendText}>Revenue</span>
</div>
</div>
</div>
<BarChart data={dashboardData.chartData} />
</div> </div>
{/* Latest Transactions */} {/* Products List */}
<div className={styles.chartCard}> <div className={styles.chartCard}>
<div className={styles.transactionsHeader}> <div className={styles.transactionsHeader}>
<h3 className={styles.transactionsTitle}>Latest Transactions</h3> <h3 className={styles.transactionsTitle}>Products</h3>
<a href="#" className={styles.seeAllLink}>see all transactions</a>
{/* 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>
<div className={styles.transactionsList}> <div className={styles.transactionsList}>
{dashboardData.latestTransactions.map((transaction) => ( {products.map((product) => (
<div key={transaction.id} className={styles.transactionItem}> <div key={product.id} className={styles.transactionItem}>
<div className={styles.transactionLeft}> <div className={styles.transactionLeft}>
<div className={styles.transactionAvatar}>
{transaction.avatar}
</div>
<div className={styles.transactionInfo}> <div className={styles.transactionInfo}>
<h4>{transaction.name}</h4> <h4>{product.name}</h4>
<p>on {transaction.date}</p> {product.children && product.children.map((child) => (
<p key={child.id}>- {child.name}</p>
))}
</div> </div>
</div> </div>
<div className={styles.transactionRight}> <div className={styles.transactionRight}>
<span className={styles.transactionAmount}> <span className={styles.transactionAmount}>
IDR {formatCurrency(transaction.amount)} IDR {formatCurrency(product.price)}
</span>
<div className={`${styles.statusIndicator} ${getStatusClass(transaction.status)}`}></div>
<span className={styles.transactionStatus}>
{transaction.status}
</span> </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> </div>
</div> </div>
</div>
{/* Bagian form create yang lama sudah DIPINDAH ke halaman/komponen baru */}
</div> </div>
); );
}; };

View File

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

View File

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

View File

@@ -26,12 +26,23 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
onMouseEnter={() => setHoveredNav(3)} onMouseEnter={() => setHoveredNav(3)}
onMouseLeave={() => setHoveredNav(null)} onMouseLeave={() => setHoveredNav(null)}
onClick={() => { onClick={() => {
if (!username) scrollToCourse(); if (!username) scrollToProduct();
else navigate('/products'); else navigate('/products');
}} }}
> >
{username ? 'MY PRODUCTS' : 'PRODUCTS'} {username ? 'MY PRODUCTS' : 'PRODUCTS'}
</a> </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> </nav>
{/* Burger Menu Button */} {/* Burger Menu Button */}

View File

@@ -1,21 +1,36 @@
// HeroSection.jsx — 2025 refresh using React-Bootstrap + CSS Module
import React from 'react'; import React from 'react';
import { Container, Row, Col, Button } from 'react-bootstrap'; import { Container, Row, Col, Button } from 'react-bootstrap';
import styles from './HeroSection.module.css';
const HeroSection = () => { const HeroSection = () => {
return ( return (
<section className="hero-section pt-5 bg-light"> <section className={`${styles.hero} pt-5`}
aria-label="Kediri Technopark hero section">
<Container> <Container>
<Row className="align-items-center"> <Row className="align-items-center gy-5">
<Col lg={6}> {/* Image on top for mobile, text first on lg+ */}
<h1>KATALIS KARIR DAN BISNIS DIGITAL</h1> <Col xs={{ order: 1 }} lg={{ span: 6, order: 1 }}>
<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={styles.copyWrap}>
<div className="d-flex gap-3"> <h1 className={styles.title}>
<Button variant="outline-primary" href="https://instagram.com/kediri.technopark" target="_blank">Instagram</Button> KATALIS KARIR DAN BISNIS DIGITAL
<Button variant="outline-success" href="tel:+6281318894994">WhatsApp</Button> </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> </div>
</Col> </Col>
<Col lg={6}> <Col xs={{ order: 0 }} lg={{ span: 6, order: 2 }}>
<img src="https://kediritechnopark.com/assets/images/gambar1.png" alt="Hero Image" className="img-fluid" /> <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> </Col>
</Row> </Row>
</Container> </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"> <section id="knowledge" className="knowledge section pt-5">
<Container> <Container>
<Row> <Row>
<Col lg={{ span: 8, offset: 2 }}> <Col >
<div className="section-heading text-center mb-4"> <div className="section-heading mb-4">
<h4>KNOWLEDGE <em>BASE</em></h4> <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> <p>Berbagai artikel dan panduan untuk membantu Anda memahami teknologi dan inovasi digital.</p>
</div> </div>
<div className="knowledge-content"> <div className="knowledge-content">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: sticky; /* position: sticky; */
top: 0; top: 0;
z-index: 1000; z-index: 1000;
} }
@@ -194,14 +194,14 @@
box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.3); box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
/* Tambahan untuk menghilangkan highlight biru */
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none; user-select: none;
outline: none; outline: none;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
} }
.courseCard:focus { .courseCard:focus {
@@ -217,7 +217,7 @@
.courseImage { .courseImage {
width: 100%; width: 100%;
height: 200px; height: 200px;
background-color: #e2e8f0; background-color: white;
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -225,7 +225,7 @@
color: #64748b; color: #64748b;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: contain;
background-position: center; background-position: center;
} }
@@ -241,8 +241,20 @@
font-weight: 600; font-weight: 600;
} }
.courseContent { .courseContentTop {
padding: 1.5rem; 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; text-align: left;
} }
@@ -490,13 +502,11 @@
.ctaTitle, .ctaTitle,
.courseTitle { .courseTitle {
font-size: 15px;
margin: 0; margin: 0;
} }
.ctaDescription, .ctaDescription,
.courseContent p { .courseContentTop, .courseContentBottom p {
font-size: 13px;
margin: 6px 0px; margin: 6px 0px;
} }
@@ -506,9 +516,8 @@
background-color: #f8fafc; background-color: #f8fafc;
} }
.courseContent { .courseContentTop, .courseContentBottom {
text-align: left; text-align: left;
padding: 0.8rem;
} }
.ctaContainer, .ctaContainer,
@@ -627,3 +636,35 @@
background-color: #2563eb; background-color: #2563eb;
color: white; 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 [showedModal, setShowedModal] = useState(null);
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
// Buka modal otomatis berdasarkan query
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const modal = urlParams.get('modal');
const productId = urlParams.get('product_id');
if (modal === 'product' && productId && products.length > 0) {
const product = products.find(p => String(p.id) === productId);
if (product) {
setSelectedProduct(product);
setShowedModal('product');
}
}
}, [products]);
useEffect(() => { useEffect(() => {
if (!subscriptions) return; if (!subscriptions) return;
// Step 1: Group subscriptions by product_name
function groupSubscriptionsByProductName(subs) { function groupSubscriptionsByProductName(subs) {
const result = {}; const result = {};
subs.forEach(sub => { subs.forEach(sub => {
@@ -30,34 +44,28 @@ const CoursePage = ({ subscriptions }) => {
}; };
} }
// Update end_date jika lebih baru
const currentEnd = new Date(result[name].end_date); const currentEnd = new Date(result[name].end_date);
const thisEnd = new Date(sub.end_date); const thisEnd = new Date(sub.end_date);
if (thisEnd > currentEnd) { if (thisEnd > currentEnd) {
result[name].end_date = sub.end_date; result[name].end_date = sub.end_date;
} }
// Tambahkan quantity jika unit_type adalah 'token'
if (sub.unit_type == 'token') { if (sub.unit_type == 'token') {
result[name].quantity += sub.quantity ?? 0; result[name].quantity += sub.quantity ?? 0;
} else { } else {
result[name].quantity += 1; // Bisa diabaikan atau tetap hitung 1 per subscription result[name].quantity += 1;
} }
result[name].subscriptions.push(sub); result[name].subscriptions.push(sub);
}); });
return result; return result;
} }
const groupedSubs = groupSubscriptionsByProductName(subscriptions); const groupedSubs = groupSubscriptionsByProductName(subscriptions);
// Step 2: Ambil semua unique product_id (tetap diperlukan untuk ambil metadata dari API)
const productIds = [...new Set(subscriptions.map(s => s.product_id))]; const productIds = [...new Set(subscriptions.map(s => s.product_id))];
// Step 3: Fetch product metadata fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -67,25 +75,27 @@ const CoursePage = ({ subscriptions }) => {
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
const enrichedData = Object.values(groupedSubs) const enrichedData = Object.values(groupedSubs)
.filter(group => data.some(p => p.id === group.product_id)) // ✅ hanya produk yang ada di metadata .filter(group => data.some(p => p.id === group.product_id))
.map(group => { .map(group => {
const productData = data.find(p => p.id == group.product_id); const productData = data.find(p => p.id == group.product_id);
// Cek fallback image dari parent jika image kosong dan sub_product_of ada
let image = productData?.image || ''; let image = productData?.image || '';
let description = productData?.description || ''; let description = productData?.description || '';
let site_url = productData?.site_url || '';
if (!image && productData?.sub_product_of) { if (!image && productData?.sub_product_of) {
const parent = data.find(p => p.id === productData.sub_product_of); const parent = data.find(p => p.id === productData.sub_product_of);
console.log(parent)
image = parent?.image || ''; image = parent?.image || '';
description = parent?.description || ''; description = parent?.description || '';
site_url = parent?.site_url || '';
} }
console.log(site_url)
return { return {
id: group.product_id, id: group.product_id,
name: group.product_name, name: group.product_name,
type: productData?.type || 'product', type: productData?.type || 'product',
image: image, image: image,
description: description, description: description,
site_url: site_url,
price: productData?.price || 0, price: productData?.price || 0,
currency: productData?.currency || 'IDR', currency: productData?.currency || 'IDR',
duration: productData?.duration || {}, duration: productData?.duration || {},
@@ -94,43 +104,19 @@ const CoursePage = ({ subscriptions }) => {
unit_type: productData?.unit_type || group.unit_type, unit_type: productData?.unit_type || group.unit_type,
quantity: group.quantity, quantity: group.quantity,
end_date: group.end_date, end_date: group.end_date,
children: [] children: [],
}; };
}); });
console.log(enrichedData) console.log(enrichedData)
setProducts(enrichedData); setProducts(enrichedData);
console.log('Enriched Data:', enrichedData);
}) })
.catch(err => console.error('Fetch error:', err)); .catch(err => console.error('Fetch error:', err));
}, [subscriptions]); }, [subscriptions]);
const features = [/* ... (tidak diubah) ... */];
const features = [
{
icon: '🌐',
title: 'Belajar Langsung dari Mentor Terbaik',
description:
'Kursus kami dirancang dan dipandu oleh para praktisi, pengajar, dan mentor yang ahli di bidangnya—mulai dari bisnis digital, teknologi, desain, hingga kecerdasan buatan. Semua materi disemakan dengan bahasa yang sederhana, mudah dipahami, dan langsung bisa dipraktikkan.',
},
{
icon: '⏰',
title: 'Fleksibel Sesuai Gaya Hidupmu',
description:
'Sibuk kerja? Urus anak? Atau lagi nyantai belajar Teknilog, di Akademi ini kamu bisa belajar kapan saja di mana saja, tanpa terikat waktu. Semua kursus kami bisa diakses ulang dan kamu bebas atur ritme belajar mu sendiri. Bebas lekukan, makamali ngatif.',
},
{
icon: '⚡',
title: 'Belajar Cepat, Dampak Nyata',
description:
'Kami percaya proses belajar tidak harus lama lama! Dengan pendekatan yang tepat, kamu bisa menguasai keterampilan baru hanya dalam hitungan minggu—buken bulan! Mulai dari belajar desain, digital marketing, AI, hingga manajemen usaha, semua bisa kamu kuasai dengan cepat dan tepat guna.',
},
];
return ( return (
<div style={{ fontFamily: 'Inter, system-ui, sans-serif' }}> <div style={{ fontFamily: 'Inter, system-ui, sans-serif' }}>
{/* Courses Section */} {/* Courses Section */}
<section className={styles.Section}> <section className={styles.Section}>
<div className={styles.coursesContainer}> <div className={styles.coursesContainer}>
@@ -138,8 +124,7 @@ const CoursePage = ({ subscriptions }) => {
<div className={styles.coursesGrid}> <div className={styles.coursesGrid}>
{products && {products &&
products[0]?.name && products[0]?.name &&
products products.map(product => (
.map(product => (
<div <div
key={product.name} key={product.name}
className={`${styles.courseCard} ${hoveredCard === product.name ? styles.courseCardHover : ''}`} className={`${styles.courseCard} ${hoveredCard === product.name ? styles.courseCardHover : ''}`}
@@ -150,14 +135,14 @@ const CoursePage = ({ subscriptions }) => {
onMouseEnter={() => setHoveredCard(product.name)} onMouseEnter={() => setHoveredCard(product.name)}
onMouseLeave={() => setHoveredCard(null)} onMouseLeave={() => setHoveredCard(null)}
> >
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}> <div>
{/* {product.price == 0 && ( <div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }} />
<span className={styles.courseLabel}>Free</span> <div className={styles.courseContentTop}>
)} */} <h3 className={styles.courseTitle}>{product.name.split('%%%')[0]}</h3>
</div>
<div className={styles.courseContent}>
<h3 className={styles.courseTitle}>{product.name}</h3>
<p className={styles.courseDesc}>{product.description}</p> <p className={styles.courseDesc}>{product.description}</p>
</div>
</div>
<div className={styles.courseContentBottom}>
<div className={styles.coursePrice}> <div className={styles.coursePrice}>
<span <span
className={ className={
@@ -168,9 +153,7 @@ const CoursePage = ({ subscriptions }) => {
> >
{product.unit_type === 'duration' {product.unit_type === 'duration'
? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : 'N/A'}` ? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : 'N/A'}`
: `SISA TOKEN ${product.quantity || 0}` : `SISA TOKEN ${product.quantity || 0}`}
}
</span> </span>
</div> </div>
</div> </div>
@@ -181,68 +164,10 @@ const CoursePage = ({ subscriptions }) => {
</section> </section>
{/* Features Section */} {/* Features Section */}
<section className={styles.Section}> {/* ... tidak berubah ... */}
<div className={styles.featuresContainer}>
<h2 className={styles.featuresTitle}>Mengapa Memilih Akademi Kami?</h2>
<p className={styles.featuresDescription}>
Di era digital yang terus berubah, Akademi kami hadir sebagai ruang tumbuh untuk siapa saja yang ingin berkembang.
Baik pelajar, profesional, UMKM, hingga pemula teknologikami bantu kamu naik level dengan materi praktis,
akses mudah, dan komunitas suportif.
</p>
<div className={styles.featuresList}>
{features.map((feature, index) => (
<div key={index} className={styles.featureItem}>
<div className={styles.featureIcon}>{feature.icon}</div>
<div className={styles.featureContent}>
<h3 className={styles.featureTitle}>{feature.title}</h3>
<p className={styles.featureDescription}>{feature.description}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className={styles.Section}>
<div className={styles.ctaContainer}>
<div className={styles.ctaCard}>
<div>
<div className={styles.ctaIcon}>😊</div>
<h3 className={styles.ctaTitle}>Murid Daftar Disini</h3>
<p className={styles.ctaDescription}>
Ambil langkah pertama menuju karier impian atau hobi barumu bersama Akademi Kami.
Belajar dengan cara yang menyenangkan, fleksibel, dan penuh manfaat.
</p>
</div>
<button className={styles.ctaButton}>START LEARNING</button>
</div>
<div className={styles.ctaCard}>
<div>
<div className={styles.ctaIcon}>👨🏫</div>
<h3 className={styles.ctaTitle}>Guru Daftar Disini</h3>
<p className={styles.ctaDescription}>
Ajarkan apa yang kamu cintai. Akademi kami memberikan semua alat
dan dukungan yang kamu butuhkan untuk membuat kursusmu sendiri.
</p>
</div>
<button className={styles.ctaButton}>START TEACHING</button>
</div>
</div>
</section>
{/* Footer */} {/* Footer */}
<footer className={styles.footer}> {/* ... tidak berubah ... */}
<div className={styles.footerContent}>
<p className={styles.footerText}>Created by Academy Kediri Techno Park</p>
<div className={styles.socialLinks}>
<a href="#" className={styles.socialLink}>📷</a>
<a href="#" className={styles.socialLink}>📱</a>
<a href="#" className={styles.socialLink}>📧</a>
</div>
</div>
</footer>
{/* Unified Modal */} {/* Unified Modal */}
{showedModal && ( {showedModal && (
@@ -253,28 +178,29 @@ const CoursePage = ({ subscriptions }) => {
setSelectedProduct({}); setSelectedProduct({});
}} }}
> >
<div <div className={styles.modalBody} onClick={(e) => e.stopPropagation()}>
className={styles.modalBody}
onClick={(e) => e.stopPropagation()}
>
{showedModal === 'product' && ( {showedModal === 'product' && (
<div>
<ProductDetailPage <ProductDetailPage
setPostLoginAction={setPostLoginAction} setPostLoginAction={setPostLoginAction}
setShowedModal={setShowedModal} setShowedModal={setShowedModal}
product={selectedProduct} product={selectedProduct}
subscriptions={subscriptions}
onClose={() => { onClose={() => {
setShowedModal(null); setShowedModal(null);
setSelectedProduct({}); setSelectedProduct({});
}} }}
/> />
</div>
)} )}
{showedModal === 'login' && ( {showedModal === 'login' && (
<Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} /> <Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} />
)} )}
</div> </div>
</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);
}