This commit is contained in:
Vassshhh
2025-08-11 16:38:35 +07:00
parent 7d3655236e
commit 82518c96aa
18 changed files with 636 additions and 417 deletions

View File

@@ -14,6 +14,7 @@ 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';
@@ -31,6 +32,8 @@ function HomePage({
return (
<>
<HeroSection />
<AboutUsSection />
<ServicesSection />
<ProductSection
productSectionRef={productSectionRef}
@@ -46,7 +49,6 @@ function HomePage({
setSelectedProduct={setSelectedProduct}
setShowedModal={setShowedModal}
/>
<AboutUsSection />
<KnowledgeBaseSection />
<ClientsSection />
</>
@@ -75,6 +77,7 @@ function App() {
const [subscriptions, setSubscriptions] = useState(null);
const [selectedProduct, setSelectedProduct] = useState({});
const [showedModal, setShowedModal] = useState(null);
const [subProductOf, setSubProductOf] = useState(null);
const [username, setUsername] = useState(null);
const productSectionRef = useRef(null);
@@ -103,7 +106,7 @@ function App() {
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',
@@ -121,7 +124,11 @@ function App() {
}
}
})
.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();
});
}
}, []);
@@ -193,7 +200,7 @@ function App() {
}
else {// Assuming you already imported processProducts from './processProducts'
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',
@@ -220,7 +227,7 @@ function App() {
window.location.href = decodeURIComponent(unauthorizedUri);
} else {
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',
@@ -304,7 +311,17 @@ function App() {
}
/>
<Route path="/products" element={<ProductsPage subscriptions={subscriptions} />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route
path="/dashboard"
element={
<Dashboard
setShowedModal={(e, productId) => {
setShowedModal(e);
setSubProductOf(productId);
}}
/>
}
/>
</Routes>
<Footer />
@@ -333,6 +350,15 @@ function App() {
setShowedModal={setShowedModal}
/>
)}
{showedModal === 'create-item' && (
<CreateProductPage
parentId={subProductOf}
subscriptions={subscriptions}
requestLogin={requestLogin}
product={selectedProduct}
setShowedModal={setShowedModal}
/>
)}
{showedModal === 'login' && (
<Login setShowedModal={setShowedModal} />
)}

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,56 +20,57 @@ 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 => (
<div
key={product.id}
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
onClick={() => {
setSelectedProduct(product);
setShowedModal('product');
}}
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.courseContentTop}>
<h3 className={styles.courseTitle}>{product.name}</h3>
<p className={styles.courseDesc}>{product.description}</p>
products
.map(product => (
<div
key={product.id}
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
onClick={() => {
setSelectedProduct(product);
setShowedModal('product');
}}
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.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={
product.price === 0
? styles.freePrice
: styles.currentPrice
}
>
{product.price == null
? 'Pay-As-You-Go'
: `Rp ${product.price.toLocaleString('id-ID')}`}
</span>
<div className={styles.courseContentBottom}>
<div className={styles.coursePrice}>
<span
className={
product.price === 0
? styles.freePrice
: styles.currentPrice
}
>
{product.price == null
? 'Pay-As-You-Go'
: `Rp ${product.price.toLocaleString('id-ID')}`}
</span>
</div>
</div>
</div>
</div>
</div>
))}
))}
</div>
</Container>
</section>

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,12 +1,15 @@
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';
const Dashboard = () => {
const [unitType, setUnitType] = useState('duration');
const [durationUnit, setDurationUnit] = useState('day');
/**
* 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);
@@ -14,24 +17,10 @@ const Dashboard = () => {
const [isVisible, setIsVisible] = useState(true);
const [products, setProducts] = useState([]);
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 },
@@ -40,48 +29,7 @@ const Dashboard = () => {
{ 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: []
});
useEffect(() => {
@@ -91,7 +39,7 @@ const Dashboard = () => {
const token = match[2];
try {
const res = await fetch('https://bot.kediritechnopark.com/webhook/store-dev/get-products', {
const res = await fetch('https://bot.kediritechnopark.com/webhook/store-production/get-products', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@@ -99,16 +47,15 @@ const Dashboard = () => {
},
});
const result = await res.json(); // hasil berupa array produk
const products = result || [];
const result = await res.json();
const productsArr = result || [];
// Ambil distinct `type` dan `group` manual
const types = [...new Set(products.map(p => p.type).filter(Boolean))];
const groups = [...new Set(products.map(p => p.group).filter(Boolean))];
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(products));
setProducts(processProducts(productsArr));
} catch (err) {
console.error('Gagal ambil produk:', err);
}
@@ -117,47 +64,6 @@ const Dashboard = () => {
fetchDistinctOptions();
}, []);
const sendDataToN8N = async (webhookUrl, data) => {
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
if (match) {
const token = match[2];
const payload = {
...data,
duration: data.unit_type === 'token' ? null : data.duration,
quantity: data.unit_type === 'duration' ? null : data.quantity,
};
if (!token) {
alert('Token tidak ditemukan. Silakan login kembali.');
return;
}
try {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
if (response.ok) {
alert('Dorm berhasil ditambahkan!');
} 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.');
}
}
};
const formatCurrency = (amount) => new Intl.NumberFormat('id-ID').format(amount);
const getStatusClass = (status) => {
@@ -221,202 +127,63 @@ const Dashboard = () => {
</div>
<div className={styles.chartsGrid}>
{/* Chart and Transactions UI as before */}
{/* Tempatkan <BarChart data={dashboardData.chartData} /> jika mau ditampilkan */}
</div>
{/* <div className={styles.chartCard}>
<div className={styles.transactionsHeader}>
<h3 className={styles.transactionsTitle}>Latest Transactions</h3>
<a href="#" className={styles.seeAllLink}>see all</a>
</div>
{/* Products List */}
<div className={styles.chartCard}>
<div className={styles.transactionsHeader}>
<h3 className={styles.transactionsTitle}>Products</h3>
<div className={styles.transactionsList}>
{products.map((transaction) => (
<div key={transaction.id} className={styles.transactionItem}>
<div className={styles.transactionLeft}>
<div className={styles.transactionInfo}>
<h4>{transaction.name}</h4>
<p>on {transaction.date}</p>
</div>
</div>
<div className={styles.transactionRight}>
<span className={styles.transactionAmount}>
IDR {formatCurrency(transaction.amount)}
</span>
<div className={`${styles.statusIndicator} ${getStatusClass(transaction.status)}`}></div>
<span className={styles.transactionStatus}>
{transaction.status}
</span>
</div>
</div>
))}
</div>
</div> */}
<div className={styles.chartCard}>
<div className={styles.transactionsHeader}>
<h3 className={styles.transactionsTitle}>Products</h3>
</div>
<div className={styles.transactionsList}>
{products.map((product) => (
<div key={product.id} className={styles.transactionItem}>
<div className={styles.transactionLeft}>
<div className={styles.transactionInfo}>
<h4>{product.name}</h4>
{product.children && product.children.map((child) => (
<p>- {child.name}</p>
))}
</div>
</div>
<div className={styles.transactionRight}>
<span className={styles.transactionAmount}>
IDR {formatCurrency(product.amount)}
</span>
<div className={`${styles.statusIndicator} ${getStatusClass(product.status)}`}></div>
<span className={styles.transactionStatus}>
{product.status}
</span>
</div>
</div>
))}
</div>
{/* 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.chartCard} style={{ marginTop: '2rem' }}>
<h3 className={styles.transactionsTitle}>Tambah Produk Baru</h3>
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target;
const isToken = unitType === 'token';
const durationValue = form.duration_value?.value;
const quantityValue = form.duration_quantity?.value;
const dormData = {
name: form.name.value,
type: selectedType,
image: form.image.value,
description: form.description.value,
price: parseInt(form.price.value, 10),
currency: 'IDR',
duration: isToken ? null : { [durationUnit]: parseInt(durationValue, 10) },
quantity: isToken ? parseInt(quantityValue, 10) : null,
unit_type: unitType,
sub_product_of: null,
is_visible: isVisible,
group: selectedGroup,
site_url: form.site_url.value || null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
<div className={styles.transactionsList}>
{products.map((product) => (
<div key={product.id} className={styles.transactionItem}>
<div className={styles.transactionLeft}>
<div className={styles.transactionInfo}>
<h4>{product.name}</h4>
{product.children && product.children.map((child) => (
<p key={child.id}>- {child.name}</p>
))}
</div>
</div>
sendDataToN8N('https://bot.kediritechnopark.com/webhook/store-dev/add-product', dormData);
}}
className={styles.form}
>
<div className={styles.formGroup}>
<label>Nama Produk</label>
<input type="text" name="name" required />
</div>
<div className={styles.formGroup}>
<label>Deskripsi</label>
<textarea name="description" rows={3} required />
</div>
<div className={styles.formGroup}>
<label>Harga</label>
<input type="number" name="price" required />
</div>
<div className={styles.formGroup}>
<label>Jenis Unit</label>
<select
name="unit_type"
value={unitType}
onChange={(e) => setUnitType(e.target.value)}
required
>
<option value="duration">Durasi</option>
<option value="token">Token</option>
</select>
</div>
<div className={styles.transactionRight}>
<span className={styles.transactionAmount}>
IDR {formatCurrency(product.price)}
</span>
<div className={`${styles.statusIndicator} ${getStatusClass(product.status)}`}></div>
<span className={styles.transactionStatus}>{product.status}</span>
{unitType === 'token' ? (
<div className={styles.formGroup}>
<label>Jumlah Token</label>
<input type="number" name="duration_quantity" required min="1" />
</div>
) : (
<div className={styles.formGroup}>
<label>Durasi</label>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<input type="number" name="duration_value" min="1" required />
<select name="duration_unit" value={durationUnit} onChange={(e) => setDurationUnit(e.target.value)} required>
<option value="day">Hari</option>
<option value="week">Minggu</option>
<option value="month">Bulan</option>
</select>
{/* 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 className={styles.formGroup}>
<label>URL Gambar</label>
<input type="text" name="image" />
</div>
<div className={styles.formGroup}>
<label>Site URL (opsional)</label>
<input type="text" name="site_url" />
</div>
<div className={styles.formGroup}>
<label>Tipe Produk</label>
<input
type="text"
name="type"
value={selectedType || ''}
onChange={(e) => setSelectedType(e.target.value)}
required
/>
<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"
name="group"
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>
<button type="submit" className={styles.submitButton}>Buat Produk</button>
</form>
))}
</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;
@@ -383,3 +386,10 @@
.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 === 3 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(3)}
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

@@ -77,7 +77,7 @@ const LoginRegister = ({setShowedModal}) => {
}
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 }),
@@ -122,7 +122,7 @@ const LoginRegister = ({setShowedModal}) => {
}
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

@@ -85,6 +85,8 @@ const ProductDetail = ({ subscriptions, product, requestLogin, setShowedModal })
}
}
setShowNamingInput(true);
return;
}
// No children, no matching subscription
@@ -156,20 +158,20 @@ const ProductDetail = ({ subscriptions, product, requestLogin, setShowedModal })
<>
<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}>
{product.end_date && product.site_url && (
{product.site_url && (
<button
className={`${styles.button} ${styles.checkoutButton}`}
onClick={() => {
const token = (document.cookie.split('; ').find(row => row.startsWith('token=')) || '').split('=')[1] || '';
const url = `${product.site_url}/dashboard/${product.name.toLowerCase().replace(/\s+/g, '_')}?token=${token}`;
window.open(url, '_blank');
const url = `https://${product.site_url}/dashboard/${product.name.split('%%%')[0]}?token=${token}`;
window.location.href = url;
}}
>
KUNJUNGI
@@ -225,7 +227,7 @@ const ProductDetail = ({ subscriptions, product, requestLogin, setShowedModal })
{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
@@ -235,7 +237,7 @@ const ProductDetail = ({ subscriptions, product, requestLogin, setShowedModal })
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>
@@ -246,7 +248,7 @@ const ProductDetail = ({ subscriptions, product, requestLogin, setShowedModal })
checked={selectedSubscriptionId === product.id}
onChange={() => setSelectedSubscriptionId(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)}>
@@ -261,7 +263,7 @@ const ProductDetail = ({ subscriptions, product, requestLogin, setShowedModal })
{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..."
@@ -286,7 +288,12 @@ const ProductDetail = ({ subscriptions, product, requestLogin, setShowedModal })
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

View File

@@ -11,7 +11,7 @@ const ProductSection = ({ hoveredCard, setHoveredCard, setSelectedProduct, setSh
// 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',
@@ -28,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>

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;
}
@@ -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;
}
@@ -636,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

@@ -65,7 +65,7 @@ const CoursePage = ({ subscriptions }) => {
const groupedSubs = groupSubscriptionsByProductName(subscriptions);
const productIds = [...new Set(subscriptions.map(s => s.product_id))];
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',
@@ -135,7 +135,7 @@ const CoursePage = ({ subscriptions }) => {
<div>
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }} />
<div className={styles.courseContentTop}>
<h3 className={styles.courseTitle}>{product.name}</h3>
<h3 className={styles.courseTitle}>{product.name.split('%%%')[0]}</h3>
<p className={styles.courseDesc}>{product.description}</p>
</div>
</div>