Compare commits
13 Commits
e3154e4cde
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3776a9559a | ||
|
|
17f5685840 | ||
|
|
c7ab5db1b5 | ||
|
|
fc934c88d8 | ||
|
|
1a84386cdc | ||
|
|
bfbb750c4d | ||
|
|
4ec28f7089 | ||
| b1ae4c5d82 | |||
| 170d3aa432 | |||
|
|
a4d6f9ae43 | ||
|
|
37c106b3bf | ||
|
|
37fca895bf | ||
|
|
56961ef8f6 |
BIN
public/assets/cafe-hore.png
Normal file
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
BIN
public/assets/kloowear.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/logo_pemprov_jatim.png
Normal file
BIN
public/assets/logo_pemprov_jatim.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
BIN
public/assets/psi.png
Normal file
BIN
public/assets/psi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
public/assets/suar.avif
Normal file
BIN
public/assets/suar.avif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
public/maya-idle.mp4
Normal file
BIN
public/maya-idle.mp4
Normal file
Binary file not shown.
48
src/App.js
48
src/App.js
@@ -38,11 +38,11 @@ function HomePage({
|
|||||||
|
|
||||||
if (tab === 'products') scrollToProduct();
|
if (tab === 'products') scrollToProduct();
|
||||||
if (tab === 'academy') scrollToCourse();
|
if (tab === 'academy') scrollToCourse();
|
||||||
}, [productSectionRef, courseSectionRef]);
|
}, [productSectionRef.current, courseSectionRef.current]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeroSection />
|
<HeroSection scrollToProduct={scrollToProduct} scrollToCourse={scrollToCourse}/>
|
||||||
|
|
||||||
<AboutUsSection />
|
<AboutUsSection />
|
||||||
<ServicesSection />
|
<ServicesSection />
|
||||||
@@ -159,7 +159,7 @@ 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
|
||||||
@@ -179,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
|
||||||
@@ -194,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,
|
||||||
@@ -227,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');
|
||||||
}
|
}
|
||||||
@@ -240,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,
|
||||||
@@ -254,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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -11,10 +11,83 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
|
|||||||
const [showSubscriptionSelector, setShowSubscriptionSelector] = useState(false);
|
const [showSubscriptionSelector, setShowSubscriptionSelector] = useState(false);
|
||||||
|
|
||||||
const [showNamingInput, setShowNamingInput] = useState(false);
|
const [showNamingInput, setShowNamingInput] = useState(false);
|
||||||
const [customName, setCustomName] = useState('');
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
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='));
|
||||||
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
|
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
|
||||||
@@ -29,7 +102,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
|
|||||||
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)
|
||||||
// ✅ Check subscription first
|
// ✅ Check subscription first
|
||||||
if (hasMatchingSubscription) {
|
if (hasMatchingSubscription) {
|
||||||
const matching = subscriptions.filter(sub =>
|
const matching = subscriptions.filter(sub =>
|
||||||
@@ -46,7 +119,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +131,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
|
|||||||
|
|
||||||
// Fallback: direct checkout
|
// 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)
|
// ✅ Confirm child selection (final step after naming)
|
||||||
@@ -74,7 +147,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
|
|||||||
const encodedName = encodeURIComponent(customName.trim() || product.name);
|
const encodedName = encodeURIComponent(customName.trim() || product.name);
|
||||||
const itemsParam = JSON.stringify(selectedChildIds);
|
const itemsParam = JSON.stringify(selectedChildIds);
|
||||||
|
|
||||||
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`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ User sets name first → then if product has children, show child selector
|
// ✅ User sets name first → then if product has children, show child selector
|
||||||
@@ -86,7 +159,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
|
|||||||
|
|
||||||
if (product.children && product.children.length > 0) {
|
if (product.children && product.children.length > 0) {
|
||||||
// don’t redirect yet → go to child selector
|
// don’t redirect yet → go to child selector
|
||||||
setShowSubscriptionSelector(false);
|
setShowSubscriptionSelector(false);
|
||||||
|
|
||||||
setShowNamingInput(false);
|
setShowNamingInput(false);
|
||||||
setShowChildSelector(true);
|
setShowChildSelector(true);
|
||||||
@@ -99,7 +172,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
|
|||||||
const itemsParam = 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 = () => {
|
||||||
@@ -121,19 +194,55 @@ 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();
|
||||||
}
|
}
|
||||||
|
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('');
|
if (setWillDo) setWillDo('');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const priceColor = product.price === 0 ? '#059669' : '#2563eb';
|
const priceColor = product.price === 0 ? '#059669' : '#2563eb';
|
||||||
|
|
||||||
|
// 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 */}
|
{/* Default view */}
|
||||||
@@ -228,6 +337,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Naming input */}
|
{/* 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>
|
||||||
@@ -236,18 +346,24 @@ 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' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.buttonGroup}>
|
<StatusLine />
|
||||||
|
|
||||||
|
<div className={styles.buttonGroup} style={{ marginTop: 12 }}>
|
||||||
<button className={styles.button} onClick={() => setShowNamingInput(false)}>
|
<button className={styles.button} onClick={() => setShowNamingInput(false)}>
|
||||||
Kembali
|
Kembali
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
onClick={onFinalCheckoutNewProduct}
|
onClick={onFinalCheckoutNewProduct}
|
||||||
disabled={customName.trim() === ''}
|
disabled={customName.trim() === '' || status !== 'available'}
|
||||||
|
title={status !== 'available' ? 'Nama belum tersedia' : 'Lanjut'}
|
||||||
>
|
>
|
||||||
Lanjut
|
Lanjut
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import styles from "../Styles.module.css";
|
import { Container, Row, Col, Card, Button, Tabs, Tab, Form, ListGroup, Badge, Accordion } from "react-bootstrap";
|
||||||
import { Box, Settings, ShoppingCart } from "lucide-react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
|
|
||||||
const Dashboard = ({
|
const Dashboard = ({
|
||||||
subscriptions,
|
subscriptions,
|
||||||
@@ -13,10 +11,9 @@ const Dashboard = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeTab, setActiveTab] = useState("products");
|
const [activeTab, setActiveTab] = useState("products");
|
||||||
const [hoveredCard, setHoveredCard] = useState(null);
|
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
|
const [hoveredCard, setHoveredCard] = useState(null);
|
||||||
|
|
||||||
// User Settings form state
|
|
||||||
const [settings, setSettings] = useState({
|
const [settings, setSettings] = useState({
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
email: "",
|
||||||
@@ -30,132 +27,30 @@ const Dashboard = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const [purchaseHistory, setPurchaseHistory] = useState([]);
|
||||||
if (userData) {
|
|
||||||
setSettings(userData);
|
|
||||||
}
|
|
||||||
}, [userData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!subscriptions) return;
|
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
||||||
|
const token = match ? match[2] : null;
|
||||||
|
|
||||||
function groupSubscriptionsByProductName(subs) {
|
if (!token) {
|
||||||
const result = {};
|
console.error("Token not found in cookies");
|
||||||
subs.forEach((sub) => {
|
return;
|
||||||
const name = sub.product_name;
|
|
||||||
const productId = sub.product_id;
|
|
||||||
if (!result[name]) {
|
|
||||||
result[name] = {
|
|
||||||
product_id: productId,
|
|
||||||
product_name: name,
|
|
||||||
unit_type: sub.unit_type,
|
|
||||||
end_date: sub.end_date,
|
|
||||||
quantity: 0,
|
|
||||||
subscriptions: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentEnd = new Date(result[name].end_date);
|
|
||||||
const thisEnd = new Date(sub.end_date);
|
|
||||||
if (thisEnd > currentEnd) {
|
|
||||||
result[name].end_date = sub.end_date;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub.unit_type === "token") {
|
|
||||||
result[name].quantity += sub.quantity ?? 0;
|
|
||||||
} else {
|
|
||||||
result[name].quantity += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
result[name].subscriptions.push(sub);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupedSubs = groupSubscriptionsByProductName(subscriptions);
|
fetch("https://bot.kediritechnopark.com/webhook/store-production/history", {
|
||||||
const productIds = [...new Set(subscriptions.map((s) => s.product_id))];
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
fetch(
|
|
||||||
"https://bot.kediritechnopark.com/webhook/store-production/products",
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ itemsId: productIds})
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => {
|
||||||
.then((data) => {
|
if (!res.ok) throw new Error("Failed to fetch purchase history");
|
||||||
// Step 1: Enrich base products (without children yet)
|
return res.json();
|
||||||
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 image = productData?.image || "";
|
|
||||||
let description = productData?.description || "";
|
|
||||||
let site_url = productData?.site_url || "";
|
|
||||||
if (!image && productData?.sub_product_of) {
|
|
||||||
const parent = data.find(
|
|
||||||
(p) => p.id === productData.sub_product_of
|
|
||||||
);
|
|
||||||
image = parent?.image || "";
|
|
||||||
description = parent?.description || "";
|
|
||||||
site_url = parent?.site_url || "";
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
executeCheckout: group.product_name,
|
|
||||||
id: group.product_id,
|
|
||||||
name: group.product_name,
|
|
||||||
type: productData?.type || "product",
|
|
||||||
image,
|
|
||||||
description,
|
|
||||||
site_url,
|
|
||||||
price: productData?.price || 0,
|
|
||||||
currency: productData?.currency || "IDR",
|
|
||||||
duration: productData?.duration || {},
|
|
||||||
sub_product_of: productData?.sub_product_of || null,
|
|
||||||
is_visible: productData?.is_visible ?? true,
|
|
||||||
unit_type: productData?.unit_type || group.unit_type,
|
|
||||||
quantity: group.quantity,
|
|
||||||
end_date: group.end_date,
|
|
||||||
children: []
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Step 2: Create a quick lookup table for enrichedData
|
|
||||||
const productMap = {};
|
|
||||||
enrichedData.forEach((p) => {
|
|
||||||
console.log(p)
|
|
||||||
productMap[p.name.split("%%%")[1]] = p;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Step 3: Find children in API `data` and attach to parents
|
|
||||||
data
|
|
||||||
.filter((p) => p.sub_product_of) // only those with a parent
|
|
||||||
.forEach((child) => {
|
|
||||||
// ✅ Current logic — attach to the real parent
|
|
||||||
const parent = productMap[child.sub_product_of];
|
|
||||||
if (parent) {
|
|
||||||
parent.children.push(child);
|
|
||||||
}
|
|
||||||
// ➕ New logic — attach to other products with the same sub_product_of
|
|
||||||
Object.values(productMap).forEach((possibleParent) => {
|
|
||||||
if (
|
|
||||||
possibleParent.id !== child.id && // not itself
|
|
||||||
possibleParent.sub_product_of === child.sub_product_of // same parent reference
|
|
||||||
) {
|
|
||||||
possibleParent.children.push(child);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log(enrichedData)
|
|
||||||
setProducts(enrichedData);
|
|
||||||
})
|
})
|
||||||
.catch((err) => console.error("Fetch error:", err));
|
.then((data) => setPurchaseHistory(data))
|
||||||
|
.catch((err) => console.error("Error fetching purchase history:", err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
}, [subscriptions]);
|
|
||||||
|
|
||||||
const handleSettingsChange = (field, value, nested = false) => {
|
const handleSettingsChange = (field, value, nested = false) => {
|
||||||
if (nested) {
|
if (nested) {
|
||||||
@@ -169,256 +64,377 @@ const Dashboard = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveSettings = () => {
|
const saveSettings = () => {
|
||||||
fetch(
|
fetch("https://bot.kediritechnopark.com/webhook-test/user-production/data", {
|
||||||
"https://bot.kediritechnopark.com/webhook-test/user-production/data",
|
method: "PUT",
|
||||||
{
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "PUT",
|
body: JSON.stringify(settings)
|
||||||
headers: {
|
})
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify(settings)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then(() => {
|
.then(() => alert("Settings updated successfully!"))
|
||||||
alert("Settings updated successfully!");
|
|
||||||
})
|
|
||||||
.catch((err) => alert("Error updating settings: " + err));
|
.catch((err) => alert("Error updating settings: " + err));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const groupSubscriptionsByProductName = (subs) => {
|
||||||
|
const result = {};
|
||||||
|
subs.forEach((sub) => {
|
||||||
|
const name = sub.product_name;
|
||||||
|
const productId = sub.product_id;
|
||||||
|
if (!result[name]) {
|
||||||
|
result[name] = {
|
||||||
|
product_id: productId,
|
||||||
|
product_name: name,
|
||||||
|
unit_type: sub.unit_type,
|
||||||
|
end_date: sub.end_date,
|
||||||
|
quantity: 0,
|
||||||
|
subscriptions: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const currentEnd = new Date(result[name].end_date);
|
||||||
|
const thisEnd = new Date(sub.end_date);
|
||||||
|
if (thisEnd > currentEnd) result[name].end_date = sub.end_date;
|
||||||
|
|
||||||
|
if (sub.unit_type === "token") {
|
||||||
|
result[name].quantity += sub.quantity ?? 0;
|
||||||
|
} else {
|
||||||
|
result[name].quantity += 1;
|
||||||
|
}
|
||||||
|
result[name].subscriptions.push(sub);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!subscriptions) return;
|
||||||
|
|
||||||
|
const groupedSubs = groupSubscriptionsByProductName(subscriptions);
|
||||||
|
const productIds = [...new Set(subscriptions.map((s) => s.product_id))];
|
||||||
|
|
||||||
|
fetch("https://bot.kediritechnopark.com/webhook/store-production/products", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ itemsId: productIds, withChildren: true })
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
const dataMap = {};
|
||||||
|
data.forEach((item) => {
|
||||||
|
dataMap[item.id] = { ...item, children: [] };
|
||||||
|
});
|
||||||
|
|
||||||
|
data.forEach((item) => {
|
||||||
|
if (item.sub_product_of && dataMap[item.sub_product_of]) {
|
||||||
|
dataMap[item.sub_product_of].children.push(dataMap[item.id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
||||||
|
executeCheckout: group.product_name,
|
||||||
|
id: group.product_id,
|
||||||
|
name: group.product_name,
|
||||||
|
realProductName,
|
||||||
|
type: productData?.type || "product",
|
||||||
|
description,
|
||||||
|
site_url,
|
||||||
|
price: productData?.price || 0,
|
||||||
|
currency: productData?.currency || "IDR",
|
||||||
|
duration: productData?.duration || {},
|
||||||
|
sub_product_of: productData?.sub_product_of || null,
|
||||||
|
is_visible: productData?.is_visible ?? true,
|
||||||
|
unit_type: productData?.unit_type || group.unit_type,
|
||||||
|
quantity: group.quantity,
|
||||||
|
end_date: group.end_date,
|
||||||
|
children: dataMap[productData?.sub_product_of]?.children || []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setProducts(enrichedData);
|
||||||
|
})
|
||||||
|
.catch((err) => console.error("Fetch error:", err));
|
||||||
|
}, [subscriptions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ fontFamily: "Inter, system-ui, sans-serif" }}>
|
<Container fluid className="py-4 px-3 px-md-5">
|
||||||
{/* Tabs Navigation */}
|
<Tabs activeKey={activeTab} onSelect={(k) => setActiveTab(k)} className="mb-3">
|
||||||
<div className={styles.navTabs}>
|
<Tab eventKey="products" title="Produk Saya">
|
||||||
<button
|
<Row>
|
||||||
className={`${styles.floatMenuItem} ${activeTab === "products" ? styles.floatMenuItemActive : ""
|
{products.map((product, i) => (
|
||||||
}`}
|
<Col md={4} key={i} className="mb-4">
|
||||||
onClick={() => setActiveTab("products")}
|
<Card
|
||||||
>
|
className={`h-100 shadow-sm p-2 ${hoveredCard === product.name ? "border-primary" : ""}`}
|
||||||
<span className={styles.floatMenuTitle}>Produk Saya</span>
|
onMouseEnter={() => setHoveredCard(product.name)}
|
||||||
<Box size={16} className={styles.floatMenuIcon} />
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`${styles.floatMenuItem} ${activeTab === "settings" ? styles.floatMenuItemActive : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setActiveTab("settings")}
|
|
||||||
>
|
|
||||||
<span className={styles.floatMenuTitle}>Profil Pengguna</span>
|
|
||||||
<Settings size={16} className={styles.floatMenuIcon} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`${styles.floatMenuItem} ${activeTab === "orders" ? styles.floatMenuItemActive : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setActiveTab("orders")}
|
|
||||||
>
|
|
||||||
<span className={styles.floatMenuTitle}>Pembelian</span>
|
|
||||||
<ShoppingCart size={16} className={styles.floatMenuIcon} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab Content */}
|
|
||||||
{activeTab === "products" && (
|
|
||||||
<section className={styles.Section}>
|
|
||||||
<div className={styles.coursesContainer}>
|
|
||||||
<div className={styles.coursesGrid}>
|
|
||||||
{products.map((product) => (
|
|
||||||
<div
|
|
||||||
key={product.name}
|
|
||||||
className={`${styles.courseCard} ${hoveredCard === product.name ? styles.courseCardHover : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedProduct(product);
|
setSelectedProduct(product);
|
||||||
setShowedModal("product");
|
setShowedModal("product");
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => setHoveredCard(product.name)}
|
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
|
||||||
>
|
>
|
||||||
<div>
|
<Card.Body>
|
||||||
<div
|
<Card.Title>
|
||||||
className={styles.courseImage}
|
📦 {product.name.split("%%%")[0] + ' | ' + product.realProductName}
|
||||||
style={{
|
</Card.Title>
|
||||||
backgroundImage: `url(${product.image})`
|
<Card.Text style={{ fontSize: "0.9rem", color: "#555" }}>
|
||||||
}}
|
{product.description}
|
||||||
/>
|
</Card.Text>
|
||||||
<div className={styles.courseContentTop}>
|
</Card.Body>
|
||||||
<h3 className={styles.courseTitle}>
|
<Card.Footer className="d-flex justify-content-between align-items-center">
|
||||||
{product.name.split("%%%")[0]}
|
<small className="text-muted">
|
||||||
</h3>
|
{product.unit_type === "duration"
|
||||||
<p className={styles.courseDesc}>
|
? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : "N/A"}`
|
||||||
{product.description}
|
: `SISA TOKEN: ${product.quantity || 0}`}
|
||||||
</p>
|
</small>
|
||||||
</div>
|
<Button
|
||||||
</div>
|
variant="outline-primary"
|
||||||
<div className={styles.courseContentBottom}>
|
size="sm"
|
||||||
<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");
|
||||||
setWillDo('checkout');
|
setWillDo("checkout");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Perpanjang
|
Perpanjang
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Card.Footer>
|
||||||
</div>
|
</Card>
|
||||||
))}
|
</Col>
|
||||||
<div
|
))}
|
||||||
className={`${styles.courseCard} ${hoveredCard === 0 ? styles.courseCardHover : ""
|
|
||||||
}`}
|
<Col md={4} className="mb-4">
|
||||||
onMouseEnter={() => setHoveredCard(0)}
|
<Card
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
className="h-100 shadow-sm d-flex justify-content-center align-items-center text-center"
|
||||||
onClick={() => navigate('/?tab=products')
|
onClick={() => navigate("/?tab=products")}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div>
|
<Card.Body>
|
||||||
+ Tambah produk baru</div>
|
<h5 style={{ color: "#007bff" }}>➕ Tambah Produk Baru</h5>
|
||||||
</div>
|
</Card.Body>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</Col>
|
||||||
</section>
|
</Row>
|
||||||
)}
|
</Tab>
|
||||||
|
|
||||||
{activeTab === "settings" && (
|
<Tab eventKey="settings" title="Profil">
|
||||||
<section className={styles.profileSection}>
|
<Card className="shadow-sm border-0">
|
||||||
<h2 className={styles.profileHeading}>Profil</h2>
|
<Card.Body>
|
||||||
<div className={styles.sectionDivider}></div>
|
<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>
|
||||||
|
|
||||||
<div className={styles.formGrid}>
|
<Row>
|
||||||
<div>
|
<Col md={6}>
|
||||||
<label className={styles.label}>Username</label>
|
<Form.Group className="mb-3">
|
||||||
<input
|
<Form.Label>Nama Lengkap</Form.Label>
|
||||||
className={styles.inputField}
|
<Form.Control
|
||||||
value={settings.username}
|
value={settings.profile_data.name}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleSettingsChange("username", e.target.value)
|
handleSettingsChange("name", e.target.value, true)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Form.Group>
|
||||||
<div>
|
</Col>
|
||||||
<label className={styles.label}>Email</label>
|
<Col md={6}>
|
||||||
<input
|
<Form.Group className="mb-3">
|
||||||
className={styles.inputField}
|
<Form.Label>No. HP</Form.Label>
|
||||||
value={settings.email}
|
<Form.Control
|
||||||
onChange={(e) =>
|
value={settings.profile_data.phone}
|
||||||
handleSettingsChange("email", e.target.value)
|
onChange={(e) =>
|
||||||
}
|
handleSettingsChange("phone", e.target.value, true)
|
||||||
/>
|
}
|
||||||
</div>
|
/>
|
||||||
<div>
|
</Form.Group>
|
||||||
<label className={styles.label}>Full Name</label>
|
</Col>
|
||||||
<input
|
</Row>
|
||||||
className={styles.inputField}
|
|
||||||
value={settings.profile_data.name}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleSettingsChange("name", e.target.value, true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={styles.label}>Phone</label>
|
|
||||||
<input
|
|
||||||
className={styles.inputField}
|
|
||||||
value={settings.profile_data.phone}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleSettingsChange("phone", e.target.value, true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.fullWidth}>
|
|
||||||
<label className={styles.label}>Address</label>
|
|
||||||
<input
|
|
||||||
className={styles.inputField}
|
|
||||||
value={settings.profile_data.address}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleSettingsChange("address", e.target.value, true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={styles.label}>Company</label>
|
|
||||||
<input
|
|
||||||
className={styles.inputField}
|
|
||||||
value={settings.profile_data.company}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleSettingsChange("company", e.target.value, true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={styles.label}>Profile Image URL</label>
|
|
||||||
<input
|
|
||||||
className={styles.inputField}
|
|
||||||
value={settings.profile_data.image}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleSettingsChange("image", e.target.value, true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className={styles.profileHeading}>Ganti password</h2>
|
<Form.Group className="mb-3">
|
||||||
<div className={styles.sectionDivider}></div>
|
<Form.Label>Alamat</Form.Label>
|
||||||
<div className={styles.formGrid}>
|
<Form.Control
|
||||||
<div>
|
as="textarea"
|
||||||
<label className={styles.label}>New Password</label>
|
rows={2}
|
||||||
<input
|
value={settings.profile_data.address}
|
||||||
className={styles.inputField}
|
onChange={(e) =>
|
||||||
type="password"
|
handleSettingsChange("address", e.target.value, true)
|
||||||
value={settings.password}
|
}
|
||||||
onChange={(e) =>
|
/>
|
||||||
handleSettingsChange("password", e.target.value)
|
</Form.Group>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={styles.label}>Re-type New Password</label>
|
|
||||||
<input
|
|
||||||
className={styles.inputField}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className={styles.saveButton} onClick={saveSettings}>
|
<Row>
|
||||||
Save Changes
|
<Col md={6}>
|
||||||
</button>
|
<Form.Group className="mb-3">
|
||||||
</section>
|
<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>
|
||||||
|
|
||||||
{activeTab === "orders" && (
|
<Row>
|
||||||
<section className={styles.Section}>
|
<Col md={6}>
|
||||||
<h2>My Orders</h2>
|
<Form.Group className="mb-3">
|
||||||
<p>Orders list will be displayed here.</p>
|
<Form.Label>Password Baru</Form.Label>
|
||||||
</section>
|
<Form.Control
|
||||||
)}
|
type="password"
|
||||||
</div>
|
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 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>
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user