Compare commits

..

16 Commits

Author SHA1 Message Date
jaya
3776a9559a ok 2025-10-09 12:38:02 +07:00
jaya
17f5685840 ok 2025-10-08 21:42:50 +07:00
everythingonblack
c7ab5db1b5 ok 2025-10-08 17:28:11 +07:00
jaya
fc934c88d8 ok 2025-09-24 16:06:06 +07:00
Vassshhhh
1a84386cdc ok 2025-09-24 14:05:15 +07:00
Vassshhh
bfbb750c4d ok 2025-08-25 03:47:47 +07:00
Vassshhh
4ec28f7089 ok 2025-08-23 16:26:47 +07:00
b1ae4c5d82 ok 2025-08-23 08:06:05 +00:00
170d3aa432 ok 2025-08-23 08:05:00 +00:00
Vassshhh
a4d6f9ae43 ok 2025-08-23 14:59:08 +07:00
Vassshhh
37c106b3bf ok 2025-08-20 20:34:52 +07:00
everythingonblack
37fca895bf ok 2025-08-19 18:52:12 +07:00
everythingonblack
56961ef8f6 ok 2025-08-19 00:34:23 +07:00
everythingonblack
e3154e4cde ok 2025-08-18 01:36:43 +07:00
everythingonblack
7add8f2864 Merge remote-tracking branch 'kediritechnopark.com/main' 2025-08-18 00:06:04 +07:00
everythingonblack
361ba6316b ok 2025-08-17 23:59:18 +07:00
22 changed files with 1143 additions and 899 deletions

BIN
public/assets/cafe-hore.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
public/assets/kloowear.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
public/assets/psi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
public/assets/suar.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
public/maya-idle.mp4 Normal file

Binary file not shown.

View File

@@ -10,9 +10,9 @@ import ProductSection from './components/ProductSection';
import AcademySection from './components/AcademySection'; import AcademySection from './components/AcademySection';
import AboutUsSection from './components/AboutUsSection'; import AboutUsSection from './components/AboutUsSection';
// KnowledgeBaseSection hidden temporarily // KnowledgeBaseSection hidden temporarily
// import KnowledgeBaseSection from './components/KnowledgeBaseSection'; import KnowledgeBaseSection from './components/KnowledgeBaseSection';
// ClientsSection hidden temporarily // ClientsSection hidden temporarily
// import ClientsSection from './components/ClientsSection'; import ClientsSection from './components/ClientsSection';
import FAQSection from './components/FAQSection'; import FAQSection from './components/FAQSection';
import Footer from './components/Footer'; import Footer from './components/Footer';
import ProductDetailPage from './components/ProductDetailPage'; import ProductDetailPage from './components/ProductDetailPage';
@@ -27,11 +27,22 @@ function HomePage({
setShowedModal, setShowedModal,
productSectionRef, productSectionRef,
courseSectionRef, courseSectionRef,
scrollToProduct,
scrollToCourse,
setWillDo setWillDo
}) { }) {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const tab = params.get('tab');
if (tab === 'products') scrollToProduct();
if (tab === 'academy') scrollToCourse();
}, [productSectionRef.current, courseSectionRef.current]);
return ( return (
<> <>
<HeroSection /> <HeroSection scrollToProduct={scrollToProduct} scrollToCourse={scrollToCourse}/>
<AboutUsSection /> <AboutUsSection />
<ServicesSection /> <ServicesSection />
@@ -47,9 +58,9 @@ function HomePage({
setShowedModal={setShowedModal} setShowedModal={setShowedModal}
setWillDo={setWillDo} setWillDo={setWillDo}
/> />
{/* <KnowledgeBaseSection /> */} <KnowledgeBaseSection />
{/* <ClientsSection /> */} <ClientsSection />
<FAQSection /> <Footer />
</> </>
); );
} }
@@ -134,6 +145,7 @@ function App() {
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const modalType = params.get('modal'); const modalType = params.get('modal');
const tab = params.get('tab');
const productId = params.get('product_id'); const productId = params.get('product_id');
const authorizedUri = params.get('authorized_uri'); const authorizedUri = params.get('authorized_uri');
const unauthorizedUri = params.get('unauthorized_uri'); const unauthorizedUri = params.get('unauthorized_uri');
@@ -147,11 +159,14 @@ function App() {
if (unauthorizedUri) localStorage.setItem('unauthorized_uri', unauthorizedUri); if (unauthorizedUri) localStorage.setItem('unauthorized_uri', unauthorizedUri);
// Jika belum login, tampilkan modal login // Jika belum login, tampilkan modal login
if (!token) { if (!token && authorizedUri) {
setShowedModal('login'); setShowedModal('login');
} }
// Jika sudah login, tidak langsung fetch di sini — akan diproses saat subscriptions tersedia // Jika sudah login, tidak langsung fetch di sini — akan diproses saat subscriptions tersedia
} }
if (tab === 'products') scrollToProduct();
if (tab === 'academy') scrollToCourse();
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -164,7 +179,7 @@ function App() {
const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2]; const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2];
if (modalType === 'product' && productId) { if (modalType === 'product' && productId) {
if (!token) { if (!token && authorizedUri) {
setShowedModal('login'); // belum login → tampilkan login modal setShowedModal('login'); // belum login → tampilkan login modal
} else { } else {
// sudah login → lanjutkan proses otorisasi saat subscriptions tersedia // sudah login → lanjutkan proses otorisasi saat subscriptions tersedia
@@ -179,31 +194,42 @@ function App() {
if (!productModalRequest || !subscriptions) return; if (!productModalRequest || !subscriptions) return;
const { productId, authorizedUri, unauthorizedUri } = productModalRequest; const { productId, authorizedUri, unauthorizedUri } = productModalRequest;
console.log(subscriptions)
const hasAccess = subscriptions && subscriptions.some( const hasAccess = subscriptions && subscriptions.some(
sub => sub.product_id === productId || sub.product_parent_id === productId sub => sub.product_id === productId || sub.product_parent_id === productId
); );
console.log(hasAccess) console.log("hasAccess:", hasAccess);
if (hasAccess) { if (hasAccess) {
if (authorizedUri) { if (authorizedUri) {
let finalUri = decodeURIComponent(authorizedUri); let finalUri = decodeURIComponent(authorizedUri);
const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2]; const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2];
if (finalUri.includes('token=null') || finalUri.includes('token=')) { // --- ambil product_name distinct berdasarkan productId/parent ---
const relatedSubs = subscriptions.filter(
sub => sub.product_id === productId || sub.product_parent_id === productId
);
const distinctNames = [...new Set(relatedSubs.map(sub => sub.product_name))];
if (distinctNames.length > 1) {
// lebih dari 1 → pakai dashboard=true
const url = new URL(finalUri); const url = new URL(finalUri);
url.searchParams.set('token', token || ''); url.searchParams.set("token", token || "");
finalUri = url.toString();
} else if (distinctNames.length === 1) {
// hanya 1 → tambahkan productName=<nama> sebelum query lain
const url = new URL(finalUri);
url.searchParams.set("token", token || "");
url.searchParams.set("productName", distinctNames[0]);
finalUri = url.toString(); finalUri = url.toString();
} }
window.location.href = finalUri; window.location.href = finalUri;
} } else {
else {// Assuming you already imported processProducts from './processProducts' // fallback ambil detail produk via fetch
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', { fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
},
body: JSON.stringify({ body: JSON.stringify({
itemsId: [productId], itemsId: [productId],
withChildren: true, withChildren: true,
@@ -212,9 +238,7 @@ function App() {
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (Array.isArray(data) && data.length > 0) { if (Array.isArray(data) && data.length > 0) {
// Process the raw data to group children under their parent
const processed = processProducts(data); const processed = processProducts(data);
// Set the first product (which should be the parent with children nested)
setSelectedProduct(processed[0]); setSelectedProduct(processed[0]);
setShowedModal('product'); setShowedModal('product');
} }
@@ -225,12 +249,9 @@ function App() {
if (unauthorizedUri) { if (unauthorizedUri) {
window.location.href = decodeURIComponent(unauthorizedUri); window.location.href = decodeURIComponent(unauthorizedUri);
} else { } else {
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', { fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
},
body: JSON.stringify({ body: JSON.stringify({
itemsId: [productId], itemsId: [productId],
withChildren: true, withChildren: true,
@@ -239,9 +260,7 @@ function App() {
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (Array.isArray(data) && data.length > 0) { if (Array.isArray(data) && data.length > 0) {
// Process the raw data to group children under their parent
const processed = processProducts(data); const processed = processProducts(data);
// Set the first product (which should be the parent with children nested)
setSelectedProduct(processed[0]); setSelectedProduct(processed[0]);
setShowedModal('product'); setShowedModal('product');
} }
@@ -303,12 +322,14 @@ function App() {
productSectionRef={productSectionRef} productSectionRef={productSectionRef}
courseSectionRef={courseSectionRef} courseSectionRef={courseSectionRef}
setWillDo={setWillDo} setWillDo={setWillDo}
scrollToProduct={scrollToProduct}
scrollToCourse={scrollToCourse}
/> />
} }
/> />
<Route path="/dashboard" element={<ProductsPage <Route path="/dashboard" element={<ProductsPage
setShowedModal={setShowedModal} setShowedModal={setShowedModal}
setSelectedProduct={setSelectedProduct} subscriptions={subscriptions} />} /> setSelectedProduct={setSelectedProduct} subscriptions={subscriptions} setWillDo={setWillDo} />} />
<Route <Route
path="/admin" path="/admin"
element={ element={
@@ -321,7 +342,6 @@ function App() {
} }
/> />
</Routes> </Routes>
<Footer />
{/* Modal */} {/* Modal */}
{showedModal && ( {showedModal && (

View File

@@ -65,10 +65,7 @@ const AcademySection = ({setSelectedProduct, setShowedModal, courseSectionRef, s
<div <div
key={product.id} key={product.id}
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`} className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
onClick={() => {
setSelectedProduct(product);
setShowedModal('product');
}}
onMouseEnter={() => setHoveredCard(product.id)} onMouseEnter={() => setHoveredCard(product.id)}
onMouseLeave={() => setHoveredCard(null)} onMouseLeave={() => setHoveredCard(null)}
> >

View File

@@ -5,10 +5,11 @@ import useInView from '../hooks/useInView';
const ClientsSection = () => { const ClientsSection = () => {
const logos = [ const logos = [
'dermalounge.jpg', 'logo_pemprov_jatim.png',
'suar.avif', 'suar.avif',
'kloowear.png', 'kloowear.webp',
'psi.png', 'psi.png',
'cafe-hore.png'
]; ];
const { ref, inView } = useInView(); const { ref, inView } = useInView();
@@ -25,7 +26,7 @@ const ClientsSection = () => {
{logos.map((logo, index) => ( {logos.map((logo, index) => (
<div className={`${styles.clientLogoWrapper} m-2`} key={index}> <div className={`${styles.clientLogoWrapper} m-2`} key={index}>
<Image <Image
src={`https://kediritechnopark.com/assets/${logo}`} src={`/assets/${logo}`}
fluid fluid
className={styles.clientLogo} className={styles.clientLogo}
/> />

View File

@@ -182,7 +182,7 @@ const CoverflowCarousel = ({ products, onCardClick }) => {
<div className={styles.cardWrapper}> <div className={styles.cardWrapper}>
<ProductCard <ProductCard
product={product} product={product}
onCardClick={(p) => { onCardClick && onCardClick(p); }} onCardClick={(p,d) => { onCardClick && onCardClick(p,d); }}
isCenter={position === 0} isCenter={position === 0}
canHover={position === 0 && animationState === 'ready' && !shiftDirection && !isDragging} canHover={position === 0 && animationState === 'ready' && !shiftDirection && !isDragging}
onCollapse={position === 0 ? collapseOverlay : undefined} onCollapse={position === 0 ? collapseOverlay : undefined}

View File

@@ -1,51 +1,79 @@
// HeroSection.jsx — 2025 refresh using React-Bootstrap + CSS Module import { Container, Row, Col, Button } from "react-bootstrap";
import React from 'react'; import styles from "./HeroSection.module.css";
import { Container, Row, Col, Button } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
import styles from './HeroSection.module.css';
const HeroSection = () => {
const navigate = useNavigate();
const goProducts = () => navigate('/products');
const goAcademy = () => navigate('/#services');
const HeroSection = ({ scrollToProduct, scrollToCourse }) => {
return ( return (
<section className={`${styles.hero} pt-3 pb-3`} <section
aria-label="Kediri Technopark hero section"> className={`${styles.hero} pt-3`}
aria-label="Kediri Technopark hero section"
>
<Container className={styles.heroContainer}> <Container className={styles.heroContainer}>
<Row className="align-items-center gy-3"> <Row className="align-items-center gy-3">
{/* Text first for mobile and desktop for clarity */} {/* Text first for mobile and desktop for clarity */}
<Col xs={{ order: 0 }} lg={{ span: 8, order: 1 }} xl={{ span: 7, order: 1 }}> <Col
xs={{ order: 0 }}
lg={{ span: 8, order: 1 }}
xl={{ span: 7, order: 1 }}
>
<div className={styles.copyWrap}> <div className={styles.copyWrap}>
<h1 className={styles.title}> <h1 className={styles.title}>KATALIS KARIR DAN BISNIS DIGITAL</h1>
KATALIS KARIR DAN BISNIS DIGITAL
</h1>
<p className={styles.lead}> <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. 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> </p>
<div className={styles.ctaGroup}> <div className={styles.ctaGroup}>
<Button className={styles.ctaPrimary} onClick={goProducts}> <Button className={styles.ctaPrimary} onClick={scrollToProduct}>
Explore Products Explore Products
</Button> </Button>
<Button variant="light" className={styles.ctaSecondary} onClick={goAcademy}> <Button
variant="light"
className={styles.ctaSecondary}
onClick={scrollToCourse}
>
View Academy View Academy
</Button> </Button>
</div> </div>
</div> </div>
</Col> </Col>
<Col xs={{ order: 1 }} lg={{ span: 4, order: 2 }} xl={{ span: 5, order: 2 }}> <Col
xs={{ order: 1 }}
lg={{ span: 4, order: 2 }}
xl={{ span: 5, order: 2 }}
>
<div className={styles.imageWrap}> <div className={styles.imageWrap}>
<div className={styles.imageFrame}> <div className={styles.imageFrame}>
<img <video
src="https://kediritechnopark.com/assets/hero.png"
alt="Ekosistem digital Kediri Technopark"
className={`img-fluid ${styles.heroImage}`} className={`img-fluid ${styles.heroImage}`}
autoPlay
muted
loop
playsInline
loading="lazy" loading="lazy"
decoding="async" decoding="async"
/> >
<source src="/maya-idle.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
</div> </div>
</div> </div>
<div className={styles.header}>
<h1>Perkenalkan Maya</h1>
<p>Asisten digital kami</p>
<Button
variant="light"
className={styles.ctaSecondary}
onClick={() =>
(window.location.href =
"https://mayagen-cs.kediritechnopark.com")
}
>
Bincang dengan Maya
</Button>
</div>
</Col> </Col>
</Row> </Row>
</Container> </Container>

View File

@@ -1,8 +1,15 @@
.hero { .hero {
position: relative; position: relative;
background: background: radial-gradient(
radial-gradient(900px 400px at 0% -10%, color-mix(in srgb, var(--brand) 12%, transparent), transparent 60%), 900px 400px at 0% -10%,
radial-gradient(800px 350px at 110% 0%, rgba(0,0,0,0.05), transparent 60%); color-mix(in srgb, var(--brand) 12%, transparent),
transparent 60%
),
radial-gradient(
800px 350px at 110% 0%,
rgba(0, 0, 0, 0.05),
transparent 60%
);
overflow: visible; overflow: visible;
min-height: clamp(300px, 40svh, 450px); min-height: clamp(300px, 40svh, 450px);
display: grid; display: grid;
@@ -25,7 +32,9 @@
pointer-events: auto; pointer-events: auto;
} }
.kickerRow { margin-bottom: .25rem; } .kickerRow {
margin-bottom: 0.25rem;
}
.kickerBadge { .kickerBadge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -45,7 +54,11 @@
letter-spacing: -0.02em; letter-spacing: -0.02em;
/* Fluid type: min 24px → max 40px, ensure 1-line on desktop */ /* Fluid type: min 24px → max 40px, ensure 1-line on desktop */
font-size: clamp(1.8rem, 2vw + 0.8rem, 2.8rem); font-size: clamp(1.8rem, 2vw + 0.8rem, 2.8rem);
background: linear-gradient(92deg, var(--text) 0%, color-mix(in srgb, var(--brand) 70%, #0f172a) 100%); background: linear-gradient(
92deg,
var(--text) 0%,
color-mix(in srgb, var(--brand) 70%, #0f172a) 100%
);
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
@@ -76,7 +89,8 @@
} }
.bulletIcon { .bulletIcon {
width: 18px; height: 18px; width: 18px;
height: 18px;
border-radius: 50%; border-radius: 50%;
border: 2px solid var(--brand); border: 2px solid var(--brand);
box-shadow: inset 0 0 0 2px #fff; box-shadow: inset 0 0 0 2px #fff;
@@ -96,8 +110,8 @@
padding: 0.45rem 0.8rem !important; padding: 0.45rem 0.8rem !important;
font-weight: 600 !important; font-weight: 600 !important;
letter-spacing: 0.02em; letter-spacing: 0.02em;
transition: background-color .16s ease, border-color .16s ease; transition: background-color 0.16s ease, border-color 0.16s ease;
margin-right: .5rem; margin-right: 0.5rem;
box-shadow: var(--shadow-neutral-s); box-shadow: var(--shadow-neutral-s);
position: relative; position: relative;
z-index: 2; z-index: 2;
@@ -117,7 +131,8 @@
padding: 0.45rem 0.8rem !important; padding: 0.45rem 0.8rem !important;
font-weight: 600 !important; font-weight: 600 !important;
letter-spacing: 0.02em; letter-spacing: 0.02em;
transition: color .16s ease, border-color .16s ease, background-color .16s ease; transition: color 0.16s ease, border-color 0.16s ease,
background-color 0.16s ease;
box-shadow: var(--shadow-neutral-s); box-shadow: var(--shadow-neutral-s);
position: relative; position: relative;
z-index: 2; z-index: 2;
@@ -150,14 +165,29 @@
pointer-events: none; pointer-events: none;
} }
.imageWrap::before, .imageWrap::after { content: none; } .imageWrap::before,
.imageWrap::after {
content: none;
}
.imageFrame { .imageFrame {
position: relative; position: relative;
border-radius: calc(var(--radius-2xl) + 6px); border-radius: calc(var(--radius-2xl) + 6px);
overflow: hidden; overflow: hidden;
mask-image: linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%); mask-image: linear-gradient(
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%); to right,
transparent 0%,
black 10%,
black 90%,
transparent 100%
);
-webkit-mask-image: linear-gradient(
to right,
transparent 0%,
black 10%,
black 90%,
transparent 100%
);
z-index: -1; z-index: -1;
pointer-events: none; pointer-events: none;
} }
@@ -166,9 +196,141 @@
display: block; display: block;
width: 100%; width: 100%;
height: auto; height: auto;
aspect-ratio: 4 / 3;
object-fit: cover; object-fit: cover;
border-radius: calc(var(--radius-2xl) - 4px); border-radius: calc(var(--radius-2xl) - 4px);
object-fit: contain;
}
.header {
position: absolute;
top: 233px;
right: 150px;
z-index: 10;
max-width: 450px;
text-align: left;
}
.header h1 {
font-size: 3.02rem;
font-weight: 700;
margin-bottom: 0.75rem;
line-height: 1.1;
letter-spacing: -0.01em;
white-space: nowrap;
background: linear-gradient(
120deg,
#0f172a 0%,
#1e3a8a 25%,
#2563eb 50%,
#1e40af 75%,
#0f172a 100%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
background-size: 200% auto;
animation: gradientFlow 5s ease-in-out infinite;
filter: drop-shadow(2px 2px 4px rgba(255, 255, 255, 0.8))
drop-shadow(-1px -1px 2px rgba(255, 255, 255, 0.6))
drop-shadow(0 3px 10px rgba(37, 99, 235, 0.2));
}
@keyframes gradientFlow {
0%,
100% {
background-position: 0% center;
}
50% {
background-position: 100% center;
}
}
.header p {
font-size: 1.125rem;
color: #475569;
margin-bottom: 1.5rem;
font-weight: 500;
text-shadow: 1px 1px 3px rgba(255, 255, 255, 0.9),
0 2px 6px rgba(0, 0, 0, 0.12);
}
.ctaSecondary {
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 2.5px solid #1e40af;
color: #1e40af;
padding: 0.875rem 2rem;
border-radius: 28px;
font-weight: 600;
font-size: 0.975rem;
cursor: pointer;
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 3px 10px rgba(30, 64, 175, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
display: inline-block;
position: relative;
z-index: 1;
}
.ctaSecondary:hover {
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #2563eb 100%);
color: white;
border-color: #1e3a8a;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(30, 58, 138, 0.4),
0 2px 8px rgba(37, 99, 235, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.ctaSecondary:active {
transform: translateY(0);
}
/* Responsive Design */
@media (max-width: 1400px) {
.header {
right: 120px;
}
}
@media (max-width: 1200px) {
.header {
right: 80px;
max-width: 400px;
}
.header h1 {
font-size: 2.75rem;
}
}
@media (max-width: 992px) {
.header {
position: static;
max-width: 100%;
text-align: center;
padding: 2rem;
margin: 0 auto;
}
.header h1 {
font-size: 2.5rem;
white-space: normal;
}
}
@media (max-width: 768px) {
.header h1 {
font-size: 2rem;
}
.header p {
font-size: 1rem;
}
.ctaSecondary {
padding: 0.75rem 1.75rem;
}
} }
.glow { .glow {
@@ -177,7 +339,11 @@
height: 40%; height: 40%;
filter: blur(40px); filter: blur(40px);
z-index: -1; z-index: -1;
background: radial-gradient(60% 60% at 50% 0%, color-mix(in srgb, var(--brand) 30%, transparent), transparent 60%); background: radial-gradient(
60% 60% at 50% 0%,
color-mix(in srgb, var(--brand) 30%, transparent),
transparent 60%
);
} }
.stats { .stats {
@@ -185,54 +351,117 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
color: #64748b; /* slate-500 */ color: #64748b;
font-size: 0.9rem; font-size: 0.9rem;
} }
.statItem strong { color: #0f172a; font-weight: 700; margin-right: 4px; } .statItem strong {
.statDot { width: 4px; height: 4px; border-radius: 2px; background: #cbd5e1; } color: #0f172a;
font-weight: 700;
margin-right: 4px;
}
.statDot {
width: 4px;
height: 4px;
border-radius: 2px;
background: #cbd5e1;
}
/* Fine-tuned responsive spacing */
@media (min-width: 992px) { @media (min-width: 992px) {
.copyWrap { padding-right: 1rem; } .copyWrap {
padding-right: 1rem;
}
} }
@media (max-width: 575.98px) { @media (max-width: 575.98px) {
.hero { padding-top: 1rem; } .hero {
.ctaGroup { display: grid; gap: 8px; } padding-top: 1rem;
.ctaPrimary, .ctaSecondary { width: 100% !important; text-align: center; } }
.copyWrap { max-width: 100%; padding: 0 10px; } .ctaGroup {
.title { font-size: clamp(1.3rem, 2.5vw + 1rem, 1.8rem); } display: grid;
.lead { font-size: clamp(0.9rem, 0.5vw + 0.8rem, 1rem); } gap: 8px;
}
.ctaPrimary,
.ctaSecondary {
width: 100% !important;
text-align: center;
}
.copyWrap {
max-width: 100%;
padding: 0 10px;
}
.title {
font-size: clamp(1.3rem, 2.5vw + 1rem, 1.8rem);
}
.lead {
font-size: clamp(0.9rem, 0.5vw + 0.8rem, 1rem);
}
} }
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
.imageWrap::before, .imageWrap::before,
.imageWrap::after { display: none; } .imageWrap::after {
.title { font-size: clamp(1.4rem, 2vw + 1rem, 2.1rem); line-height: 1.12; } display: none;
.lead { font-size: clamp(0.93rem, 0.4vw + 0.84rem, 1.03rem); } }
.bulletItem { font-size: 0.92rem; } .title {
.mesh, .grid { display: none; } font-size: clamp(1.4rem, 2vw + 1rem, 2.1rem);
.copyWrap { max-width: 100%; padding: 0 15px; } line-height: 1.12;
.imageWrap { max-width: 100%; } }
.imageFrame { border-radius: calc(var(--radius-2xl) + 2px); } .lead {
.heroImage { border-radius: calc(var(--radius-2xl) - 6px); } font-size: clamp(0.93rem, 0.4vw + 0.84rem, 1.03rem);
}
.bulletItem {
font-size: 0.92rem;
}
.mesh,
.grid {
display: none;
}
.copyWrap {
max-width: 100%;
padding: 0 15px;
}
.imageWrap {
max-width: 100%;
}
.imageFrame {
border-radius: calc(var(--radius-2xl) + 2px);
}
.heroImage {
border-radius: calc(var(--radius-2xl) - 6px);
}
} }
.imageFrame:hover { box-shadow: none; transform: none; } .imageFrame:hover {
box-shadow: none;
transform: none;
}
@media (min-width: 1400px) { @media (min-width: 1400px) {
.imageWrap { max-width: 720px; } .imageWrap {
max-width: 720px;
}
} }
@media (max-width: 400px) { @media (max-width: 400px) {
.hero { padding-top: 0.8rem; } .hero {
.title { font-size: clamp(1.2rem, 3vw + 0.9rem, 1.7rem); } padding-top: 0.8rem;
.lead { font-size: clamp(0.85rem, 0.5vw + 0.75rem, 0.95rem); } }
.ctaGroup { gap: 6px; } .title {
.ctaPrimary, .ctaSecondary { font-size: clamp(1.2rem, 3vw + 0.9rem, 1.7rem);
}
.lead {
font-size: clamp(0.85rem, 0.5vw + 0.75rem, 0.95rem);
}
.ctaGroup {
gap: 6px;
}
.ctaPrimary,
.ctaSecondary {
padding: 0.4rem 0.7rem !important; padding: 0.4rem 0.7rem !important;
font-size: 0.9rem; font-size: 0.9rem;
} }
.copyWrap { padding: 0 5px; } .copyWrap {
padding: 0 5px;
}
} }

View File

@@ -105,6 +105,10 @@ const LoginRegister = ({setShowedModal}) => {
window.history.replaceState({}, '', newUrl); window.history.replaceState({}, '', newUrl);
setShowedModal('product'); setShowedModal('product');
} else { } else {
const params = new URLSearchParams(window.location.search);
const modalType = params.get('modal');
if(!modalType)
navigate('/dashboard'); navigate('/dashboard');
window.location.reload(); window.location.reload();
} }

View File

@@ -33,16 +33,16 @@ const ProductCard = ({ product, onCardClick, isCenter, canHover, onCollapse }) =
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<button <button
className={styles.detailButton} className={styles.detailButton}
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onCardClick && onCardClick(product); }} onClick={(e) => { e.preventDefault(); e.stopPropagation(); onCardClick && onCardClick(product, true); }}
> >
Detail Detail
</button> </button>
<button {/* <button
className={styles.buyButton} className={styles.buyButton}
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onCardClick && onCardClick(product); }} onClick={(e) => { e.preventDefault(); e.stopPropagation(); onCardClick && onCardClick(product); }}
> >
Beli Beli
</button> </button> */}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -18,6 +18,8 @@
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
transition: transform 0.45s ease, filter 0.45s ease; transition: transform 0.45s ease, filter 0.45s ease;
background-color: #f1f1f1;
object-fit: contain;
} }
.canHover:hover .cover { .canHover:hover .cover {

View File

@@ -1,16 +1,92 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import styles from './ProductDetail.module.css'; import styles from './ProductDetail.module.css';
import { useNavigate } from 'react-router-dom';
const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin, setShowedModal }) => { const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin, setShowedModal }) => {
const [showChildSelector, setShowChildSelector] = useState(false); const [showChildSelector, setShowChildSelector] = useState(false);
const [selectedChildIds, setSelectedChildIds] = useState([]); const [selectedChildIds, setSelectedChildIds] = useState([]);
const [matchingSubscriptions, setMatchingSubscriptions] = useState([]); const [matchingSubscriptions, setMatchingSubscriptions] = useState([]);
const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(null); const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(0);
const [showSubscriptionSelector, setShowSubscriptionSelector] = useState(false); const [showSubscriptionSelector, setShowSubscriptionSelector] = useState(false);
const [showNamingInput, setShowNamingInput] = useState(false); const [showNamingInput, setShowNamingInput] = useState(false);
const navigate = useNavigate();
const [customName, setCustomName] = useState(''); const [customName, setCustomName] = useState('');
const [status, setStatus] = useState('idle'); // 'idle' | 'checking' | 'available' | 'unavailable' | 'error'
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
// Helper panggil API kamu (GET + token header)
async function checkProductAvailability(name, token) {
const url = `https://bot.kediritechnopark.com/webhook/store_production/check_p_availability?productId=${product.id}&name=${encodeURIComponent(name)}`;
const res = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
});
if (!res.ok) throw new Error(`Server error ${res.status}`);
const data = await res.json(); // expected: { allowed: true|false }
return Boolean(data.allowed);
}
// Auto check saat user mengetik (debounce)
useEffect(() => {
const name = customName.trim();
if (!name) {
setStatus('idle');
return;
}
const isNameTaken = subscriptions?.some(sub => {
const isSameProduct = sub.product_id === product.id || sub.product_parent_id === product.id;
if (!isSameProduct) return false;
const existingName = sub.product_name?.split('%%%')[0]?.trim().toLowerCase();
return existingName === name.toLowerCase();
});
if (subscriptions && isNameTaken) {
setStatus('unavailable');
return;
}
else if(!product.unique_name){
setStatus('available');
return;
}
if (!product.unique_name) {
console.log(subscriptions);
return;
}
let cancelled = false;
setStatus('checking');
const t = setTimeout(async () => {
try {
const allowed = await checkProductAvailability(name, token);
if (cancelled) return;
setStatus(allowed ? 'available' : 'unavailable');
} catch (e) {
if (cancelled) return;
setStatus('error');
}
}, 500);
return () => {
cancelled = true;
clearTimeout(t);
};
}, [customName, token]);
const onCheckout = () => { const onCheckout = () => {
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token=')); const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
@@ -20,34 +96,14 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
requestLogin('checkout'); requestLogin('checkout');
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 =>
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id) String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
); );
console.log(hasMatchingSubscription)
// Always show children selector first if product has children // ✅ Check subscription first
if (product.children && product.children.length > 0) {
setShowChildSelector(true);
if (hasMatchingSubscription) {
const matching = subscriptions.filter(sub =>
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
);
if (matching.length > 0) {
// ✅ Select only the first for each product_name
const uniqueByName = Array.from(
new Map(matching.map(sub => [sub.product_name, sub])).values()
);
setMatchingSubscriptions(uniqueByName);
}
}
return;
}
// No children, but has subscription match
if (hasMatchingSubscription) { if (hasMatchingSubscription) {
const matching = subscriptions.filter(sub => const matching = subscriptions.filter(sub =>
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id) String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
@@ -61,59 +117,62 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
setMatchingSubscriptions(uniqueByName); setMatchingSubscriptions(uniqueByName);
setShowSubscriptionSelector(true); setShowSubscriptionSelector(true);
return; return;
} } else {
else {
const itemsParam = JSON.stringify([product.id]); 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`; window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${product.name}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
return; return;
} }
} }
// ✅ If no subscription → ask for new product name
setShowNamingInput(true); setShowNamingInput(true);
return; return;
} }
// No children, no matching subscription
// Fallback: direct checkout
const itemsParam = JSON.stringify([product.id]); const itemsParam = JSON.stringify([product.id]);
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`; window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
}; };
// ✅ Confirm child selection (final step after naming)
const onConfirmChildren = () => { const onConfirmChildren = () => {
if (matchingSubscriptions.length > 0) { if (selectedChildIds.length === 0) {
setShowChildSelector(false); alert('Pilih minimal satu produk');
setShowSubscriptionSelector(true);
return;
}
else {
setShowChildSelector(false);
setShowNamingInput(true);
return; 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] : '';
if (selectedChildIds.length === 0) { const encodedName = encodeURIComponent(customName.trim() || product.name);
alert('Pilih minimal satu produk'); const itemsParam = JSON.stringify(selectedChildIds);
return;
}
const itemsParam = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : JSON.stringify([product.id]); window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
}; };
// ✅ User sets name first → then if product has children, show child selector
const onFinalCheckoutNewProduct = () => { const onFinalCheckoutNewProduct = () => {
if (!customName.trim()) { if (!customName.trim()) {
alert('Nama produk tidak boleh kosong'); alert('Nama produk tidak boleh kosong');
return; return;
} }
if (product.children && product.children.length > 0) {
// dont redirect yet → go to child selector
setShowSubscriptionSelector(false);
setShowNamingInput(false);
setShowChildSelector(true);
return;
}
// if no children → go straight to checkout
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] : '';
const itemsParam = selectedChildIds.length > 0 ? JSON.stringify(selectedChildIds) : JSON.stringify([product.id]); const itemsParam = JSON.stringify([product.id]);
const encodedName = encodeURIComponent(customName.trim()); const encodedName = encodeURIComponent(customName.trim());
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`; window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
}; };
const onConfirmSelector = () => { const onConfirmSelector = () => {
@@ -135,21 +194,58 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
const productName = selectedSubscription?.product_name; const productName = selectedSubscription?.product_name;
const encodedName = encodeURIComponent(productName); const encodedName = encodeURIComponent(productName);
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`; window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${encodedName}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
} }
}; };
useEffect(() => { useEffect(() => {
if (willDo === 'checkout') { if (!product.executeCheckout && willDo === 'checkout') {
onCheckout(); onCheckout();
} }
if(setWillDo) setWillDo(''); // Reset willDo after handling else if (product.children && product.children.length > 0) {
setShowChildSelector(true);
}
else {
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
const encodedName = encodeURIComponent(product.name);
const itemsParam = JSON.stringify([product.id]);
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${encodedName}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
}
if (setWillDo) setWillDo('');
}, []); }, []);
const priceColor = product.price === 0 ? '#059669' : '#2563eb'; const priceColor = product.price === 0 ? '#059669' : '#2563eb';
console.log(product)
// Komponen kecil untuk menampilkan status teks
const StatusLine = () => {
if (status === 'idle') return null;
const map = {
checking: 'Memeriksa…',
available: 'Nama tersedia',
unavailable: 'Nama tidak tersedia',
error: 'Gagal memeriksa. Coba lagi.',
};
const color =
status === 'available'
? '#16a34a'
: status === 'unavailable'
? '#dc2626'
: status === 'checking'
? '#2563eb'
: '#6b7280';
return (
<div style={{ marginTop: 6, fontSize: 12, color }}>
{map[status]}
</div>
);
};
return ( return (
<div className={styles.container}> <div className={styles.container}>
{/* Default view */}
{!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>
@@ -182,38 +278,30 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
sub.product_id === product.id || sub.product_parent_id === product.id sub.product_id === product.id || sub.product_parent_id === product.id
) && product.end_date ? 'Perpanjang' : 'Checkout'} ) && product.end_date ? 'Perpanjang' : 'Checkout'}
</button> </button>
</div> </div>
</> </>
)} )}
{/* Child selector */}
{showChildSelector && ( {showChildSelector && (
<div className={styles.childSelector}> <div className={styles.childSelector}>
<h3>Pilih Paket</h3> <h3>Pilih Paket</h3>
{product.children.map(child => ( {product.children.map(child => (
<label key={child.id} className={styles.childProduct}> <label key={child.id} className={styles.childProduct}>
<input <input
type="checkbox" type="radio"
value={child.id} value={child.id}
checked={selectedChildIds.includes(child.id)} checked={selectedChildIds.includes(child.id)}
onChange={e => { onChange={() => setSelectedChildIds([child.id])}
const checked = e.target.checked;
setSelectedChildIds(prev =>
checked ? [...prev, child.id] : prev.filter(id => id !== child.id)
);
}}
/> />
&nbsp;{child.name} Rp {parseInt(child.price || 0).toLocaleString('id-ID')} <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>&nbsp;{child.name}</div>
<div>Rp {parseInt(child.price || 0).toLocaleString('id-ID')}</div>
</div>
</label> </label>
))} ))}
<p>
<strong>Total Harga:</strong> Rp {selectedChildIds
.map(id => product.children.find(child => child.id === id)?.price || 0)
.reduce((a, b) => a + b, 0)
.toLocaleString('id-ID')}
</p>
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<button className={styles.button} onClick={() => setShowChildSelector(false)}> <button className={styles.button} onClick={() => { setShowChildSelector(false); setShowNamingInput(true); }}>
Kembali Kembali
</button> </button>
<button className={styles.button} onClick={onConfirmChildren}> <button className={styles.button} onClick={onConfirmChildren}>
@@ -223,42 +311,33 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
</div> </div>
)} )}
{/* Subscription selector */}
{showSubscriptionSelector && !showNamingInput && ( {showSubscriptionSelector && !showNamingInput && (
<div className={styles.childSelector}> <div className={styles.childSelector}>
<h5>Perpanjang {product.name.split('%%%')[0]} </h5> <h5>Kamu sudah punya produk ini</h5>
{matchingSubscriptions.map(sub => ( <div className={styles.childProduct} onClick={() => { setShowedModal(''); navigate('/dashboard') }}>
<label key={sub.id} className={styles.childProduct}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<input <div>Perpanjang produk ini</div>
type="radio" <div></div>
name="subscription" </div>
value={sub.id} </div>
checked={selectedSubscriptionId == sub.id} <h6>Atau</h6>
onChange={() => { setSelectedSubscriptionId(sub.id); setCustomName(sub.product_name) }} <label className={styles.childProduct} onClick={() => { setSelectedSubscriptionId(0); onConfirmSelector(); }}>
/> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
&nbsp;{sub.product_name.split('%%%')[0]} <div>Tambah {product.name.split('%%%')[0]} baru</div>
</label> <div></div>
))} </div>
<h6>Atau buat baru</h6>
<label className={styles.childProduct}>
<input
type="radio"
name="subscription"
checked={selectedSubscriptionId === 0}
onChange={() => {setSelectedSubscriptionId(0); console.log(product.id)}}
/>
&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)}>
Kembali Kembali
</button> </button>
<button className={styles.button} onClick={onConfirmSelector}>
Lanjut ke Checkout
</button>
</div> </div>
</div> </div>
)} )}
{/* Naming input */}
{showNamingInput && ( {showNamingInput && (
<div className={styles.childSelector}> <div className={styles.childSelector}>
<h5>Buat {product.name.split('%%%')[0]} Baru</h5> <h5>Buat {product.name.split('%%%')[0]} Baru</h5>
@@ -267,51 +346,30 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
placeholder="Nama produk..." placeholder="Nama produk..."
className={styles.input} className={styles.input}
value={customName} value={customName}
onChange={(e) => setCustomName(e.target.value)} onChange={(e) => {
style={{ width: '100%', padding: '8px', marginBottom: '16px', borderRadius: '10px' }} const value = e.target.value.replace(/\s+/g, '-'); // Ganti spasi dengan -
setCustomName(value);
}}
style={{ width: '100%', padding: '8px', marginBottom: '8px', borderRadius: '10px' }}
/> />
{ <StatusLine />
matchingSubscriptions.some(
(sub) => sub.product_name === `${product.name}@${customName}`
) && (
<p style={{ color: 'red', marginBottom: '10px' }}>
Nama produk sudah digunakan.
</p>
)
}
<div className={styles.buttonGroup}> <div className={styles.buttonGroup} style={{ marginTop: 12 }}>
<button <button className={styles.button} onClick={() => setShowNamingInput(false)}>
className={styles.button}
onClick={() => {
setShowNamingInput(false);
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
</button> </button>
<button <button
className={styles.button} className={styles.button}
onClick={onFinalCheckoutNewProduct} onClick={onFinalCheckoutNewProduct}
disabled={ disabled={customName.trim() === '' || status !== 'available'}
customName.trim() === '' || title={status !== 'available' ? 'Nama belum tersedia' : 'Lanjut'}
matchingSubscriptions.some(
(sub) => sub.product_name === `${product.name}@${customName}`
)
}
> >
Checkout Lanjut
</button> </button>
</div> </div>
</div> </div>
)} )}
</div> </div>
); );
}; };

View File

@@ -41,7 +41,12 @@ const ProductSection = ({ setSelectedProduct, setShowedModal, productSectionRef,
}, [selectedCategory, products]); }, [selectedCategory, products]);
// Handle product selection for detail view // Handle product selection for detail view
const handleViewDetail = (product) => { const handleViewDetail = (product, detailed) => {
console.log(product, detailed)
if(detailed) {
window.location.href = product.site_landing_url;
return;
}
setSelectedProduct(product); setSelectedProduct(product);
setShowedModal('product'); setShowedModal('product');
setWillDo('checkout'); setWillDo('checkout');

View File

@@ -91,7 +91,6 @@
.carouselContainer { .carouselContainer {
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
padding: 0 56px;
min-height: 380px; /* compact */ min-height: 380px; /* compact */
} }
@@ -133,7 +132,6 @@
} }
.carouselContainer { .carouselContainer {
padding: 0 60px;
min-height: 420px; min-height: 420px;
} }
@@ -167,7 +165,6 @@
} }
.carouselContainer { .carouselContainer {
padding: 0 50px;
min-height: 400px; min-height: 400px;
} }
@@ -210,7 +207,6 @@
} }
.carouselContainer { .carouselContainer {
padding: 0 40px;
min-height: 370px; min-height: 370px;
} }
@@ -253,7 +249,6 @@
} }
.carouselContainer { .carouselContainer {
padding: 0 30px;
min-height: 320px; min-height: 320px;
} }
@@ -296,7 +291,6 @@
} }
.carouselContainer { .carouselContainer {
padding: 0 25px;
min-height: 300px; min-height: 300px;
} }

View File

@@ -421,7 +421,7 @@
} }
.currentPrice { .currentPrice {
font-size: 0.95rem; font-size: 1.2rem;
font-weight: bold; font-weight: bold;
color: #2563eb; color: #2563eb;
} }
@@ -950,3 +950,139 @@
max-width: 150px; /* biar logo tidak terlalu besar */ max-width: 150px; /* biar logo tidak terlalu besar */
height: auto; height: auto;
} }
/* Navigation Tabs */
.navTabs {
display: flex;
align-items: center;
background-color: #2a4fd6; /* blue bar like screenshot */
padding: 0;
border-radius: 6px 6px 0 0;
overflow: hidden;
}
.floatMenuItem {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 12px 16px;
font-size: 14px;
font-weight: 500;
color: #ffffffcc; /* light white text */
background: transparent;
border: none;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
.floatMenuItem:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
.floatMenuItemActive {
background-color: #ffffff;
color: #2a4fd6; /* active text color */
font-weight: 600;
}
.floatMenuTitle {
display: inline-block;
}
.floatMenuIcon {
stroke-width: 2;
}
/* Sections */
.Section {
padding: 24px;
background: #fff;
border-radius: 0 0 6px 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* Settings Page Layout */
.profileSection {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.profileHeading {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.sectionDivider {
height: 2px;
background-color: #2a4fd6;
width: 40px;
margin: 8px 0 20px 0;
}
.formGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
background-color: #f6f9fc;
padding: 20px;
border-radius: 8px;
}
.fullWidth {
grid-column: span 2;
}
.label {
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 6px;
display: block;
}
.inputField,
.selectField {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
background-color: #fff;
font-size: 14px;
}
.selectField {
appearance: none;
}
.saveButton {
background-color: #2a4fd6;
color: white;
font-size: 14px;
font-weight: 500;
padding: 10px 24px;
border-radius: 50px;
border: none;
cursor: pointer;
margin-top: 16px;
transition: background 0.2s ease;
}
.saveButton:hover {
background-color: #223fa9;
}
/* Change Password Section */
.changePasswordSection {
margin-top: 32px;
}
.buttonRow {
display: flex;
gap: 8px;
}

View File

@@ -1,499 +0,0 @@
import React from 'react';
const KedaiMasterLanding = () => {
const styles = {
container: {
fontFamily: 'Arial, sans-serif',
margin: 0,
padding: 0,
background: 'linear-gradient(135deg, #e8f5e8 0%, #f0f8ff 100%)',
minHeight: '100vh'
},
header: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '1rem 2rem',
backgroundColor: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(10px)'
},
logo: {
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
fontSize: '1.2rem',
fontWeight: 'bold',
color: '#2c3e50'
},
nav: {
display: 'flex',
gap: '2rem',
listStyle: 'none',
margin: 0,
padding: 0
},
navLink: {
textDecoration: 'none',
color: '#2c3e50',
fontSize: '0.9rem',
transition: 'color 0.3s'
},
ctaButton: {
backgroundColor: '#4a90e2',
color: 'white',
padding: '0.5rem 1.5rem',
border: 'none',
borderRadius: '25px',
fontSize: '0.9rem',
cursor: 'pointer',
transition: 'transform 0.3s'
},
hero: {
display: 'flex',
alignItems: 'center',
padding: '4rem 2rem',
maxWidth: '1200px',
margin: '0 auto'
},
heroContent: {
flex: 1,
paddingRight: '2rem'
},
heroTitle: {
fontSize: '3rem',
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: '1rem',
lineHeight: '1.2'
},
heroText: {
fontSize: '1.1rem',
color: '#666',
lineHeight: '1.6',
marginBottom: '2rem'
},
heroImage: {
flex: 1,
textAlign: 'center'
},
coffeeIcon: {
fontSize: '8rem',
background: 'linear-gradient(45deg, #ffa726, #ff9800)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
padding: '2rem',
borderRadius: '20px',
backgroundColor: 'rgba(255, 167, 38, 0.1)'
},
features: {
padding: '4rem 2rem',
maxWidth: '1200px',
margin: '0 auto'
},
featuresTitle: {
textAlign: 'center',
fontSize: '2.5rem',
color: '#2c3e50',
marginBottom: '3rem'
},
featuresGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '2rem',
marginBottom: '4rem'
},
featureCard: {
backgroundColor: 'rgba(255, 255, 255, 0.3)',
padding: '2rem',
borderRadius: '15px',
textAlign: 'center',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.2)',
transition: 'transform 0.3s'
},
featureTitle: {
fontSize: '1.3rem',
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: '1rem'
},
featureText: {
color: '#666',
lineHeight: '1.5'
},
appShowcase: {
display: 'flex',
alignItems: 'center',
gap: '3rem',
marginTop: '4rem'
},
appContent: {
flex: 1
},
appTitle: {
fontSize: '1.5rem',
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: '1rem'
},
appText: {
color: '#666',
lineHeight: '1.6'
},
appImages: {
flex: 1,
position: 'relative',
height: '300px'
},
phoneScreen: {
width: '200px',
height: '350px',
backgroundColor: 'white',
borderRadius: '25px',
padding: '1rem',
boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
position: 'absolute',
display: 'flex',
flexDirection: 'column',
gap: '0.5rem'
},
phoneScreen1: {
left: '0',
top: '0',
zIndex: 2
},
phoneScreen2: {
right: '0',
top: '50px',
zIndex: 1
},
screenHeader: {
height: '40px',
backgroundColor: '#f0f0f0',
borderRadius: '10px',
marginBottom: '0.5rem'
},
screenContent: {
flex: 1,
backgroundColor: '#f8f8f8',
borderRadius: '10px',
padding: '0.5rem'
},
cta: {
textAlign: 'center',
padding: '4rem 2rem',
backgroundColor: 'rgba(255, 255, 255, 0.2)',
backdropFilter: 'blur(10px)'
},
ctaTitle: {
fontSize: '2rem',
color: '#2c3e50',
marginBottom: '2rem'
},
ctaButtonLarge: {
backgroundColor: '#8b4513',
color: 'white',
padding: '1rem 2rem',
border: 'none',
borderRadius: '30px',
fontSize: '1.1rem',
cursor: 'pointer',
transition: 'transform 0.3s'
},
pricing: {
padding: '4rem 2rem',
background: 'linear-gradient(135deg, #ffa726 0%, #ff9800 100%)',
textAlign: 'center'
},
pricingTitle: {
fontSize: '2.5rem',
color: 'white',
marginBottom: '3rem'
},
pricingCards: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '2rem',
maxWidth: '1000px',
margin: '0 auto'
},
pricingCard: {
backgroundColor: 'rgba(255, 255, 255, 0.95)',
padding: '2rem',
borderRadius: '15px',
position: 'relative'
},
pricingBadge: {
position: 'absolute',
top: '-10px',
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: '#4caf50',
color: 'white',
padding: '0.5rem 1rem',
borderRadius: '20px',
fontSize: '0.8rem'
},
pricingPlan: {
fontSize: '1.3rem',
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: '1rem'
},
pricingPrice: {
fontSize: '2rem',
fontWeight: 'bold',
color: '#4caf50',
marginBottom: '1.5rem'
},
pricingFeatures: {
listStyle: 'none',
padding: 0,
marginBottom: '2rem'
},
pricingFeature: {
padding: '0.5rem 0',
color: '#666',
borderBottom: '1px solid #eee'
},
pricingButton: {
backgroundColor: '#4caf50',
color: 'white',
padding: '0.8rem 2rem',
border: 'none',
borderRadius: '25px',
cursor: 'pointer',
width: '100%',
fontSize: '1rem'
},
footer: {
background: 'linear-gradient(135deg, #2196f3 0%, #21cbf3 100%)',
color: 'white',
padding: '4rem 2rem 2rem',
position: 'relative',
overflow: 'hidden'
},
footerWave: {
position: 'absolute',
top: '-50px',
left: 0,
width: '100%',
height: '100px',
background: 'rgba(255, 255, 255, 0.1)',
borderRadius: '50% 50% 0 0'
},
footerContent: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '3rem',
maxWidth: '1200px',
margin: '0 auto',
position: 'relative',
zIndex: 1
},
footerSection: {
textAlign: 'left'
},
footerTitle: {
fontSize: '1.5rem',
marginBottom: '1rem'
},
footerText: {
lineHeight: '1.6',
opacity: 0.9
},
copyright: {
textAlign: 'center',
marginTop: '2rem',
paddingTop: '2rem',
borderTop: '1px solid rgba(255, 255, 255, 0.2)',
opacity: 0.7
}
};
return (
<div style={styles.container}>
{/* Header */}
<header style={styles.header}>
<div style={styles.logo}>
<span>🏪</span>
<span>TECHNORAMA</span>
</div>
<nav>
<ul style={styles.nav}>
<li><a href="#" style={styles.navLink}>Home</a></li>
<li><a href="#" style={styles.navLink}>Services</a></li>
<li><a href="#" style={styles.navLink}>Product</a></li>
<li><a href="#" style={styles.navLink}>Academy</a></li>
<li><a href="#" style={styles.navLink}>About</a></li>
<li><a href="#" style={styles.navLink}>Contact</a></li>
</ul>
</nav>
<button style={styles.ctaButton}>Sign Up Now</button>
</header>
{/* Hero Section */}
<section style={styles.hero}>
<div style={styles.heroContent}>
<h1 style={styles.heroTitle}>Kedai Master</h1>
<p style={styles.heroText}>
Platform Point of Sale terdepan yang dirancang khusus untuk meningkatkan
kepuasan operational kafe dan restoran milik KM. Dengan sistem yang fleksibel,
terpercaya, dan efisien.
</p>
</div>
<div style={styles.heroImage}>
<div style={styles.coffeeIcon}></div>
</div>
</section>
{/* Features Section */}
<section style={styles.features}>
<h2 style={styles.featuresTitle}>Fitur Unggulan</h2>
<div style={styles.featuresGrid}>
<div style={styles.featureCard}>
<h3 style={styles.featureTitle}>Manajemen Tenant & Kasir</h3>
<p style={styles.featureText}>
Sistem untuk mengatur dan mengoptimalkan kinerja seluruh tenant.
</p>
</div>
<div style={styles.featureCard}>
<h3 style={styles.featureTitle}>QR Pemesanan di Meja</h3>
<p style={styles.featureText}>
Tamu restoran langsung dan mengoptimalkan waktu pemesanan dan pelayanan.
</p>
</div>
<div style={styles.featureCard}>
<h3 style={styles.featureTitle}>Otomatisasi Pesanan & Keuangan</h3>
<p style={styles.featureText}>
Mengatur operasional anda dengan otomatisasi pesanan dan keuangan.
</p>
</div>
<div style={styles.featureCard}>
<h3 style={styles.featureTitle}>Request & Voting Lagu</h3>
<p style={styles.featureText}>
Tamu dapat meminta lagu untuk diputar di restoran dan memberikan suasana.
</p>
</div>
</div>
{/* App Showcase */}
<div style={styles.appShowcase}>
<div style={styles.appContent}>
<h3 style={styles.appTitle}>
Gak perlu repot anti jam kerja yang baik bozen lagi.
Tinggal scan QR yang ada di meja, langsung bisa udah langsung workflow
</h3>
<p style={styles.appText}>
© 2025 KEDAIMASTERPBM.COM
</p>
</div>
<div style={styles.appImages}>
<div style={{...styles.phoneScreen, ...styles.phoneScreen1}}>
<div style={styles.screenHeader}></div>
<div style={styles.screenContent}></div>
</div>
<div style={{...styles.phoneScreen, ...styles.phoneScreen2}}>
<div style={styles.screenHeader}></div>
<div style={styles.screenContent}></div>
</div>
</div>
</div>
<div style={{...styles.appShowcase, flexDirection: 'row-reverse', marginTop: '4rem'}}>
<div style={styles.appContent}>
<h3 style={styles.appTitle}>Desain Menu Modern</h3>
<p style={styles.appText}>
Tampilan menu yang familiar, menarik dan mudah dipahami sehingga customer bisa dengan mudah memahami visual yang menarik untuk pengalaman ordering yang maksimal untuk kafe dan restoran masa kini.
</p>
</div>
<div style={styles.appImages}>
<div style={{...styles.phoneScreen, ...styles.phoneScreen1}}>
<div style={styles.screenHeader}></div>
<div style={styles.screenContent}></div>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section style={styles.cta}>
<h2 style={styles.ctaTitle}>Siap Tingkatkan Bisnis Anda?</h2>
<button style={styles.ctaButtonLarge}>Coba Kedai Master Sekarang</button>
</section>
{/* Pricing Section */}
<section style={styles.pricing}>
<h2 style={styles.pricingTitle}>OUR PACK KEDAI MASTER</h2>
<div style={styles.pricingCards}>
<div style={styles.pricingCard}>
<div style={styles.pricingBadge}>PAKET BASIC</div>
<h3 style={styles.pricingPlan}>Starter Pack</h3>
<div style={styles.pricingPrice}>Rp 245.000</div>
<ul style={styles.pricingFeatures}>
<li style={styles.pricingFeature}>1 user untuk admin</li>
<li style={styles.pricingFeature}>Support via email</li>
</ul>
<button style={styles.pricingButton}>Pilih Paket</button>
</div>
<div style={styles.pricingCard}>
<div style={styles.pricingBadge}>PAKET SILVER</div>
<h3 style={styles.pricingPlan}>Business Pack</h3>
<div style={styles.pricingPrice}>Rp 499.000</div>
<ul style={styles.pricingFeatures}>
<li style={styles.pricingFeature}>Integrasi Meja & Jemput</li>
<li style={styles.pricingFeature}>All permission & control</li>
<li style={styles.pricingFeature}>Unlimited locations for pemasangan</li>
</ul>
<button style={styles.pricingButton}>Pilih Paket</button>
</div>
<div style={styles.pricingCard}>
<div style={styles.pricingBadge}>PAKET GOLD</div>
<h3 style={styles.pricingPlan}>Enterprise Pack</h3>
<div style={styles.pricingPrice}>Rp 849.000</div>
<ul style={styles.pricingFeatures}>
<li style={styles.pricingFeature}>All benefits unlimited fitures &</li>
<li style={styles.pricingFeature}>Multi outlet & multi users</li>
<li style={styles.pricingFeature}>Integrasi fitur locations</li>
</ul>
<button style={styles.pricingButton}>Pilih Paket</button>
</div>
</div>
</section>
{/* Footer */}
<footer style={styles.footer}>
<div style={styles.footerWave}></div>
<div style={styles.footerContent}>
<div style={styles.footerSection}>
<h3 style={styles.footerTitle}>Contact Us</h3>
<p style={styles.footerText}>
Jalan ABC No. 123, Kota Surabaya, Jawa Timur 60123<br/>
Phone: +62 123 456 7890<br/>
Email: info@kedaimaster.com<br/>
Website: www.kedaimaster.com
</p>
</div>
<div style={styles.footerSection}>
<h3 style={styles.footerTitle}>About Our Company</h3>
<div style={styles.logo}>
<span>🏪</span>
<span>TECHNORAMA</span>
</div>
<p style={styles.footerText}>
Kami adalah perusahaan yang berfokus pada solusi teknologi untuk industri F&B.
Dengan pengalaman bertahun-tahun, kami berkomitmen memberikan layanan terbaik.
</p>
</div>
</div>
<div style={styles.copyright}>
<p>© 2025 Kedai Master by Technorama. All rights reserved.</p>
</div>
</footer>
</div>
);
};
export default KedaiMasterLanding;

View File

@@ -1,33 +1,82 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import ProductDetailPage from '../ProductDetailPage'; import { Container, Row, Col, Card, Button, Tabs, Tab, Form, ListGroup, Badge, Accordion } from "react-bootstrap";
import Login from '../Login'; import { useNavigate } from "react-router-dom";
import styles from '../Styles.module.css';
const CoursePage = ({ subscriptions, setSelectedProduct, setShowedModal}) => { const Dashboard = ({
const [hoveredCard, setHoveredCard] = useState(null); subscriptions,
setSelectedProduct,
setShowedModal,
userData,
setWillDo
}) => {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("products");
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
const [hoveredCard, setHoveredCard] = useState(null);
// Buka modal otomatis berdasarkan query const [settings, setSettings] = useState({
useEffect(() => { username: "",
const urlParams = new URLSearchParams(window.location.search); email: "",
const modal = urlParams.get('modal'); password: "",
const productId = urlParams.get('product_id'); profile_data: {
name: "",
image: "",
phone: "",
address: "",
company: ""
}
});
if (modal === 'product' && productId && products.length > 0) { const [purchaseHistory, setPurchaseHistory] = useState([]);
const product = products.find(p => String(p.id) === productId);
if (product) {
setSelectedProduct(product);
setShowedModal('product');
}
}
}, [products]);
useEffect(() => { useEffect(() => {
if (!subscriptions) return; const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
const token = match ? match[2] : null;
function groupSubscriptionsByProductName(subs) { if (!token) {
console.error("Token not found in cookies");
return;
}
fetch("https://bot.kediritechnopark.com/webhook/store-production/history", {
headers: {
Authorization: `Bearer ${token}`
}
})
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch purchase history");
return res.json();
})
.then((data) => setPurchaseHistory(data))
.catch((err) => console.error("Error fetching purchase history:", err));
}, []);
const handleSettingsChange = (field, value, nested = false) => {
if (nested) {
setSettings((prev) => ({
...prev,
profile_data: { ...prev.profile_data, [field]: value }
}));
} else {
setSettings((prev) => ({ ...prev, [field]: value }));
}
};
const saveSettings = () => {
fetch("https://bot.kediritechnopark.com/webhook-test/user-production/data", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(settings)
})
.then((res) => res.json())
.then(() => alert("Settings updated successfully!"))
.catch((err) => alert("Error updating settings: " + err));
};
const groupSubscriptionsByProductName = (subs) => {
const result = {}; const result = {};
subs.forEach(sub => { subs.forEach((sub) => {
const name = sub.product_name; const name = sub.product_name;
const productId = sub.product_id; const productId = sub.product_id;
if (!result[name]) { if (!result[name]) {
@@ -40,133 +89,353 @@ const CoursePage = ({ subscriptions, setSelectedProduct, setShowedModal}) => {
subscriptions: [] subscriptions: []
}; };
} }
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;
}
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; result[name].quantity += 1;
} }
result[name].subscriptions.push(sub); result[name].subscriptions.push(sub);
}); });
return result; return result;
} };
useEffect(() => {
if (!subscriptions) return;
const groupedSubs = groupSubscriptionsByProductName(subscriptions); const groupedSubs = groupSubscriptionsByProductName(subscriptions);
const productIds = [...new Set(subscriptions.map(s => s.product_id))]; const productIds = [...new Set(subscriptions.map((s) => s.product_id))];
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', { fetch("https://bot.kediritechnopark.com/webhook/store-production/products", {
method: 'POST', method: "POST",
headers: { headers: { "Content-Type": "application/json" },
'Content-Type': 'application/json', body: JSON.stringify({ itemsId: productIds, withChildren: true })
},
body: JSON.stringify({ itemsId: productIds }),
}) })
.then(res => res.json()) .then((res) => res.json())
.then(data => { .then((data) => {
const enrichedData = Object.values(groupedSubs) const dataMap = {};
.filter(group => data.some(p => p.id === group.product_id)) data.forEach((item) => {
.map(group => { dataMap[item.id] = { ...item, children: [] };
const productData = data.find(p => p.id == group.product_id); });
let image = productData?.image || '';
let description = productData?.description || ''; data.forEach((item) => {
let site_url = productData?.site_url || ''; if (item.sub_product_of && dataMap[item.sub_product_of]) {
if (!image && productData?.sub_product_of) { dataMap[item.sub_product_of].children.push(dataMap[item.id]);
const parent = data.find(p => p.id === productData.sub_product_of);
console.log(parent)
image = parent?.image || '';
description = parent?.description || '';
site_url = parent?.site_url || '';
} }
console.log(site_url) });
const enrichedData = Object.values(groupedSubs)
.filter((group) => data.some((p) => p.id === group.product_id))
.map((group) => {
const productData = data.find((p) => p.id === group.product_id);
let description = "";
console.log(productData)
let realProductName = productData?.name || "";
let site_url = productData?.site_url || "";
if (productData?.sub_product_of) {
const parent = data.find((p) => p.id === productData.sub_product_of);
realProductName = parent.name;
description = parent?.description || "";
site_url = parent?.site_url || "";
}
return { return {
executeCheckout: group.product_name,
id: group.product_id, id: group.product_id,
name: group.product_name, name: group.product_name,
type: productData?.type || 'product', realProductName,
image: image, type: productData?.type || "product",
description: description, description,
site_url: 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 || {},
sub_product_of: productData?.sub_product_of || null, sub_product_of: productData?.sub_product_of || null,
is_visible: productData?.is_visible ?? true, is_visible: productData?.is_visible ?? true,
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: dataMap[productData?.sub_product_of]?.children || []
}; };
}); });
console.log(enrichedData)
setProducts(enrichedData); setProducts(enrichedData);
}) })
.catch(err => console.error('Fetch error:', err)); .catch((err) => console.error("Fetch error:", err));
}, [subscriptions]); }, [subscriptions]);
const features = [/* ... (tidak diubah) ... */];
return ( return (
<div style={{ fontFamily: 'Inter, system-ui, sans-serif' }}> <Container fluid className="py-4 px-3 px-md-5">
{/* Courses Section */} <Tabs activeKey={activeTab} onSelect={(k) => setActiveTab(k)} className="mb-3">
<section className={styles.Section}> <Tab eventKey="products" title="Produk Saya">
<div className={styles.coursesContainer}> <Row>
<h2 className={styles.coursesTitle}>MY PRODUCTS</h2> {products.map((product, i) => (
<div className={styles.coursesGrid}> <Col md={4} key={i} className="mb-4">
{products && <Card
products[0]?.name && className={`h-100 shadow-sm p-2 ${hoveredCard === product.name ? "border-primary" : ""}`}
products.map(product => (
<div
key={product.name}
className={`${styles.courseCard} ${hoveredCard === product.name ? styles.courseCardHover : ''}`}
onClick={() => {
setSelectedProduct(product);
setShowedModal('product');
}}
onMouseEnter={() => setHoveredCard(product.name)} onMouseEnter={() => setHoveredCard(product.name)}
onMouseLeave={() => setHoveredCard(null)} onMouseLeave={() => setHoveredCard(null)}
>
<div>
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }} />
<div className={styles.courseContentTop}>
<h3 className={styles.courseTitle}>{product.name.split('%%%')[0]}</h3>
<p className={styles.courseDesc}>{product.description}</p>
</div>
</div>
<div className={styles.courseContentBottom}>
<div className={styles.coursePrice}>
<span
className={
product.price == 0
? styles.freePrice
: styles.currentPrice
}
>
{product.unit_type === 'duration'
? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : 'N/A'}`
: `SISA TOKEN ${product.quantity || 0}`}
</span>
</div>
<button className="px-4 py-2 rounded-pill text-white" style={{ background: 'linear-gradient(to right, #6a59ff, #8261ee)', border: 'none' }}
onClick={() => { onClick={() => {
setSelectedProduct(product); setSelectedProduct(product);
setShowedModal('product'); setShowedModal("product");
}}>Perpanjang</button> }}
</div> >
</div> <Card.Body>
<Card.Title>
📦 &nbsp; {product.name.split("%%%")[0] + ' | ' + product.realProductName}
</Card.Title>
<Card.Text style={{ fontSize: "0.9rem", color: "#555" }}>
{product.description}
</Card.Text>
</Card.Body>
<Card.Footer className="d-flex justify-content-between align-items-center">
<small className="text-muted">
{product.unit_type === "duration"
? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : "N/A"}`
: `SISA TOKEN: ${product.quantity || 0}`}
</small>
<Button
variant="outline-primary"
size="sm"
onClick={() => {
setSelectedProduct(product);
setShowedModal("product");
setWillDo("checkout");
}}
>
Perpanjang
</Button>
</Card.Footer>
</Card>
</Col>
))} ))}
<Col md={4} className="mb-4">
<Card
className="h-100 shadow-sm d-flex justify-content-center align-items-center text-center"
onClick={() => navigate("/?tab=products")}
>
<Card.Body>
<h5 style={{ color: "#007bff" }}> Tambah Produk Baru</h5>
</Card.Body>
</Card>
</Col>
</Row>
</Tab>
<Tab eventKey="settings" title="Profil">
<Card className="shadow-sm border-0">
<Card.Body>
<Card.Title className="mb-4">Pengaturan Profil</Card.Title>
<Form>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Username</Form.Label>
<Form.Control
value={settings.username}
onChange={(e) =>
handleSettingsChange("username", e.target.value)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Email</Form.Label>
<Form.Control
value={settings.email}
onChange={(e) =>
handleSettingsChange("email", e.target.value)
}
/>
</Form.Group>
</Col>
</Row>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Nama Lengkap</Form.Label>
<Form.Control
value={settings.profile_data.name}
onChange={(e) =>
handleSettingsChange("name", e.target.value, true)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>No. HP</Form.Label>
<Form.Control
value={settings.profile_data.phone}
onChange={(e) =>
handleSettingsChange("phone", e.target.value, true)
}
/>
</Form.Group>
</Col>
</Row>
<Form.Group className="mb-3">
<Form.Label>Alamat</Form.Label>
<Form.Control
as="textarea"
rows={2}
value={settings.profile_data.address}
onChange={(e) =>
handleSettingsChange("address", e.target.value, true)
}
/>
</Form.Group>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Perusahaan</Form.Label>
<Form.Control
value={settings.profile_data.company}
onChange={(e) =>
handleSettingsChange("company", e.target.value, true)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>URL Gambar Profil</Form.Label>
<Form.Control
value={settings.profile_data.image}
onChange={(e) =>
handleSettingsChange("image", e.target.value, true)
}
/>
</Form.Group>
</Col>
</Row>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Password Baru</Form.Label>
<Form.Control
type="password"
value={settings.password}
onChange={(e) =>
handleSettingsChange("password", e.target.value)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Ketik Ulang Password</Form.Label>
<Form.Control type="password" />
</Form.Group>
</Col>
</Row>
<Button variant="success" onClick={saveSettings}>
Simpan Perubahan
</Button>
</Form>
</Card.Body>
</Card>
</Tab>
<Tab eventKey="orders" title="Pembelian">
<Card className="shadow-sm border-0">
<Card.Body>
<Card.Title className="mb-4">Riwayat Pembelian</Card.Title>
{purchaseHistory.length === 0 ? (
<p className="text-muted">Tidak ada riwayat pembelian.</p>
) : (
<Accordion defaultActiveKey={null} alwaysOpen>
{purchaseHistory
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
.map((order, idx) => {
const createdAt = new Date(order.created_at);
// Konversi status ke label yang lebih ramah pengguna
const statusLabel = {
failed: "Transaksi Dibatalkan",
pending: "Menunggu Pembayaran",
completed: "Sukses",
}[order.status] || order.status;
// Tentukan warna/status visual
const statusVariant = {
failed: "danger",
pending: "warning",
completed: "success",
}[order.status] || "secondary";
return (
<Accordion.Item eventKey={String(idx)} key={order.transaction_id + "-" + order.detailed_transactions_id}>
<Accordion.Header>
<div className="d-flex flex-column flex-md-row justify-content-between w-100">
<div>
<strong>{order.product_name.replace(/\t/g, " ")}</strong>
<div className="text-muted small">
ID: {order.transaction_id} {createdAt.toLocaleString("id-ID")}
</div> </div>
</div> </div>
</section> <div className="text-md-end mt-2 mt-md-0">
<Badge bg={statusVariant} className="text-capitalize">
{statusLabel}
</Badge>
<div>
<strong className="ms-2">Rp {order.amount.toLocaleString("id-ID")}</strong>
</div> </div>
</div>
</div>
</Accordion.Header>
<Accordion.Body>
<Row>
<Col md={6}>
<p className="mb-1"><strong>Metode Pembayaran:</strong> {order.payment_method || "N/A"}</p>
<p className="mb-1"><strong>Status:</strong> {statusLabel}</p>
<p className="mb-1"><strong>Tanggal Transaksi:</strong> {createdAt.toLocaleString("id-ID")}</p>
</Col>
<Col md={6} className="text-md-end">
{order.status == 'failed' && (
<div>
<p className="text-danger small">
Jika Anda sudah melakukan pembayaran, silakan konfirmasi manual.
</p>
<Button
variant="outline-danger"
size="sm"
onClick={() => {
alert("Fungsi konfirmasi manual belum diimplementasikan.");
}}
>
Konfirmasi Manual
</Button>
</div>
)}
</Col>
</Row>
</Accordion.Body>
</Accordion.Item>
);
})}
</Accordion>
)}
</Card.Body>
</Card>
</Tab>
</Tabs>
</Container>
); );
}; };
export default CoursePage; export default Dashboard;