Compare commits
33 Commits
8c0b9568e8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3776a9559a | ||
|
|
17f5685840 | ||
|
|
c7ab5db1b5 | ||
|
|
fc934c88d8 | ||
|
|
1a84386cdc | ||
|
|
bfbb750c4d | ||
|
|
4ec28f7089 | ||
| b1ae4c5d82 | |||
| 170d3aa432 | |||
|
|
a4d6f9ae43 | ||
|
|
37c106b3bf | ||
|
|
37fca895bf | ||
|
|
56961ef8f6 | ||
|
|
e3154e4cde | ||
|
|
7add8f2864 | ||
|
|
361ba6316b | ||
|
|
8d677eda1d | ||
|
|
cd5fb36279 | ||
|
|
9e23588ec6 | ||
|
|
0dc6d64e07 | ||
|
|
c9d2e8946b | ||
|
|
4dd03969b3 | ||
|
|
01d8fb7bac | ||
|
|
32cff2df1f | ||
|
|
9639d6c2de | ||
|
|
8fd2661cf6 | ||
|
|
f577a23d10 | ||
|
|
82518c96aa | ||
|
|
7d3655236e | ||
|
|
3b7707a024 | ||
|
|
3cf86829ed | ||
|
|
f2b30f515c | ||
|
|
59ab35c332 |
70
gemini-context.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Gemini CLI Interaction Context
|
||||||
|
|
||||||
|
This document summarizes the key tasks and file modifications performed during the interaction with the Gemini CLI agent.
|
||||||
|
|
||||||
|
## 1. Initial Setup & Context
|
||||||
|
- Acknowledged project context and folder structure.
|
||||||
|
|
||||||
|
## 2. Product Section Redesign (Initial - Clean & Professional Look)
|
||||||
|
- **Goal:** Redesign "Our Products" section for a clean, professional look, and scalability.
|
||||||
|
- **Files Modified:**
|
||||||
|
- `src/components/ProductSection.module.css`: Created new CSS module for the section.
|
||||||
|
- `src/components/ProductCard.module.css`: Created new CSS module for individual product cards.
|
||||||
|
- `src/components/ProductCard.js`: Created new `ProductCard` component.
|
||||||
|
- `src/components/ProductSection.js`: Modified to use `ProductCard` and a new grid layout.
|
||||||
|
|
||||||
|
## 3. Product Section Redesign (Iteration 2 - Filters & Enhanced Hover)
|
||||||
|
- **Goal:** Add filtering capabilities and enhance hover effects on product cards.
|
||||||
|
- **Files Modified:**
|
||||||
|
- `src/components/ProductSection.module.css`: Added filter styles.
|
||||||
|
- `src/components/ProductCard.module.css`: Added category tags and enhanced hover effects (image zoom, button slide-up).
|
||||||
|
- `src/components/ProductCard.js`: Included category tags.
|
||||||
|
- `src/components/ProductSection.js`: Added filtering logic and filter buttons.
|
||||||
|
|
||||||
|
## 4. Copywriting Redesign (AIDA Framework)
|
||||||
|
- **Goal:** Apply AIDA framework to the section's main heading and description.
|
||||||
|
- **Files Modified:**
|
||||||
|
- `src/components/ProductSection.js`: Updated main heading and description.
|
||||||
|
|
||||||
|
## 5. Remove "Pelatihan" Category
|
||||||
|
- **Goal:** Remove "Pelatihan" (Training) category from the product section.
|
||||||
|
- **Files Modified:**
|
||||||
|
- `src/components/ProductSection.js`: Removed "Pelatihan" from categories/filters and updated copywriting.
|
||||||
|
|
||||||
|
## 6. Product Card Redesign (Overlay on Hover)
|
||||||
|
- **Goal:** Implement a full-card image with an overlay on hover for product details and CTAs.
|
||||||
|
- **Files Modified:**
|
||||||
|
- `src/components/ProductCard.module.css`: Rewrote for overlay-on-hover design.
|
||||||
|
- `src/components/ProductCard.js`: Implemented new overlay structure.
|
||||||
|
|
||||||
|
## 7. Carousel Implementation (Custom)
|
||||||
|
- **Goal:** Change product display to a custom carousel with a centered active item.
|
||||||
|
- **Files Modified:**
|
||||||
|
- `src/components/ProductSection.module.css`: Added custom carousel styles.
|
||||||
|
- `src/components/ProductSection.js`: Implemented custom carousel logic.
|
||||||
|
|
||||||
|
## 8. Carousel Implementation (React-Slick)
|
||||||
|
- **Goal:** Refactor to use the existing `react-slick` library for the carousel.
|
||||||
|
- **Files Modified:**
|
||||||
|
- `src/components/ProductSection.module.css`: Updated to use `react-slick` specific styles.
|
||||||
|
- `src/components/ProductSection.js`: Refactored to use `react-slick` component, removing custom carousel logic.
|
||||||
|
|
||||||
|
## 9. Carousel Tuning (Size & Centering Iterations)
|
||||||
|
- **Goal:** Adjust carousel size, centering, and visibility of navigation.
|
||||||
|
- **Files Modified:**
|
||||||
|
- `src/components/ProductSection.module.css`: Multiple updates to `slidesToShow`, scaling, arrow positioning, and adding/removing `carouselWrapper`.
|
||||||
|
- `src/components/ProductSection.js`: Multiple updates to `sliderSettings`.
|
||||||
|
|
||||||
|
## 10. Interactive Gallery Design
|
||||||
|
- **Goal:** Implement a completely new "Interactive Gallery" design (featured product + thumbnail navigation).
|
||||||
|
- **Files Modified:**
|
||||||
|
- `src/components/ProductSection.module.css`: Completely rewrote for gallery layout.
|
||||||
|
- `src/components/ProductSection.js`: Heavily refactored to remove `react-slick` and implement gallery logic. `ProductCard.js` was effectively deprecated in this step.
|
||||||
|
|
||||||
|
## 11. Minimalist Masonry Grid (Current Task)
|
||||||
|
- **Goal:** Implement a "Minimalist Masonry Grid" layout for product display.
|
||||||
|
- **Files Modified:**
|
||||||
|
- `src/components/ProductCard.module.css`: Modified to revert to a simple, always-visible card design suitable for masonry.
|
||||||
|
- `src/components/ProductCard.js`: (Pending user approval for modification)
|
||||||
|
- `src/components/ProductSection.module.css`: (Pending modification for masonry grid styles)
|
||||||
|
- `src/components/ProductSection.js`: (Pending modification for masonry grid logic)
|
||||||
2809
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
|
"framer-motion": "^12.23.12",
|
||||||
"lucide-react": "^0.536.0",
|
"lucide-react": "^0.536.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
"react-router-dom": "^7.7.1",
|
"react-router-dom": "^7.7.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-slick": "^0.30.3",
|
"react-slick": "^0.30.3",
|
||||||
|
"react-spring": "^10.0.1",
|
||||||
"slick-carousel": "^1.8.1",
|
"slick-carousel": "^1.8.1",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
public/assets/cafe-hore.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
5
public/assets/images/heading-line-dec.png
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="80" height="4" viewBox="0 0 80 4" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="40" height="4" fill="#6a59ff"/>
|
||||||
|
<rect x="40" width="40" height="4" fill="#0057b8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
8
public/assets/images/placeholder-product.png
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="100%" height="100%" fill="#f1f5f9"/>
|
||||||
|
<rect x="100" y="75" width="200" height="150" fill="#e2e8f0" rx="8"/>
|
||||||
|
<circle cx="200" cy="120" r="30" fill="#cbd5e1"/>
|
||||||
|
<rect x="150" y="170" width="100" height="20" fill="#cbd5e1" rx="4"/>
|
||||||
|
<rect x="170" y="200" width="60" height="15" fill="#cbd5e1" rx="4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 430 B |
BIN
public/assets/kloowear.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/logo_pemprov_jatim.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
public/assets/psi.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/assets/suar.avif
Normal file
|
After Width: | Height: | Size: 50 KiB |
@@ -3,13 +3,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Web site created using create-react-app"
|
content="Kediri Technopark - Katalis Karir dan Bisnis Digital"
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
|
||||||
<!--
|
<!--
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
@@ -24,7 +26,7 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>React App</title>
|
<title>Kediri Technopark</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
BIN
public/maya-idle.mp4
Normal file
623
public/pages/KEDAIMASTER.html
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
|
<meta name="description" content="Kedai Master - Platform Point of Sale untuk kafe & restoran" />
|
||||||
|
<meta name="author" content="Kediri Technopark" />
|
||||||
|
<link rel="icon" href="assets/images/kediri-technopark-logo.png" />
|
||||||
|
|
||||||
|
<title>Kedai Master | Kediri Technopark</title>
|
||||||
|
|
||||||
|
<!-- Fonts & Icons -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous" />
|
||||||
|
|
||||||
|
<!-- Bootstrap (CDN) -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- Base Styles -->
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--brand:#92a103;
|
||||||
|
--brand-2:#5c5a02;
|
||||||
|
--ink:#023047;
|
||||||
|
--bg:linear-gradient(90deg,#e4f0a3 0%,#e5f6ff 100%);
|
||||||
|
--gold:#ffd02a;
|
||||||
|
}
|
||||||
|
html{scroll-behavior:smooth}
|
||||||
|
body{
|
||||||
|
margin:0;
|
||||||
|
font-family: "Roboto", system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
|
||||||
|
color:var(--ink);
|
||||||
|
background:var(--bg);
|
||||||
|
overflow-x:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.app-header{
|
||||||
|
position:sticky; top:0; z-index:1000; background:#ffffffcc; backdrop-filter:saturate(180%) blur(8px);
|
||||||
|
border-bottom:1px solid rgba(0,0,0,.06);
|
||||||
|
}
|
||||||
|
.nav-brand img{height:44px}
|
||||||
|
.nav-toggle{border:none;background:transparent;font-size:1.5rem;display:none}
|
||||||
|
.nav-links{
|
||||||
|
display:flex; gap:1rem; align-items:center;
|
||||||
|
}
|
||||||
|
.nav-links a{
|
||||||
|
text-decoration:none; color:var(--ink); font-weight:500;
|
||||||
|
padding:.5rem .75rem; border-radius:.5rem;
|
||||||
|
}
|
||||||
|
.nav-links a.active,.nav-links a:hover{background:rgba(146,161,3,.12)}
|
||||||
|
.profile-wrapper{position:relative; display:inline-block;}
|
||||||
|
.profile-image{width:40px;height:40px;border-radius:50%;object-fit:cover;cursor:pointer;border:2px solid rgba(0,0,0,.05)}
|
||||||
|
.tooltip-box{
|
||||||
|
display:none; position:absolute; right:0; margin-top:.5rem;
|
||||||
|
background:#fff; border:1px solid rgba(0,0,0,.08); border-radius:.75rem;
|
||||||
|
box-shadow:0 10px 30px rgba(0,0,0,.08); padding:.75rem; min-width:180px;
|
||||||
|
}
|
||||||
|
.signout-button{
|
||||||
|
width:100%; border:none; background:#ef4444; color:#fff; padding:.5rem .75rem;
|
||||||
|
border-radius:.5rem; font-weight:600; cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero */
|
||||||
|
.hero{
|
||||||
|
display:grid; grid-template-columns:1.1fr .9fr; align-items:center; gap:3rem;
|
||||||
|
padding:clamp(2rem, 4vw, 4rem) 0;
|
||||||
|
}
|
||||||
|
.hero h1{
|
||||||
|
font-size:clamp(1.8rem, 4vw, 3rem);
|
||||||
|
background: linear-gradient(90deg,var(--brand-2), var(--brand));
|
||||||
|
-webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; margin-bottom:1rem;
|
||||||
|
}
|
||||||
|
.hero p{font-size:clamp(1rem, 1.4vw, 1.15rem); color:#244; line-height:1.7}
|
||||||
|
.move-image img{
|
||||||
|
width:100%; max-width:640px; border-radius:20px; box-shadow:0 0 50px rgba(2,4,5,.12);
|
||||||
|
}
|
||||||
|
.floating-image{animation:float 3s ease-in-out infinite}
|
||||||
|
@keyframes float {
|
||||||
|
0%{ transform: translateY(18px) }
|
||||||
|
50%{ transform: translateY(-10px) }
|
||||||
|
100%{ transform: translateY(18px) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Features */
|
||||||
|
.features{padding:2rem 0}
|
||||||
|
.features h2,.pricing-title{
|
||||||
|
text-align:center; font-weight:800; letter-spacing:.3px;
|
||||||
|
font-size:clamp(1.35rem,3.5vw,2.2rem);
|
||||||
|
}
|
||||||
|
.feature-grid{
|
||||||
|
display:grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap:1rem;
|
||||||
|
}
|
||||||
|
.feature{
|
||||||
|
background: radial-gradient(ellipse at center, rgba(120,161,5,.18), transparent 70%);
|
||||||
|
border-radius:14px; padding:1.25rem; border:1px solid rgba(146,161,3,.2);
|
||||||
|
box-shadow:0 4px 18px rgba(0,0,0,.06); transition: transform .2s ease, box-shadow .2s ease;
|
||||||
|
}
|
||||||
|
.feature:hover{ transform:translateY(-4px); box-shadow:0 8px 28px rgba(0,0,0,.08) }
|
||||||
|
.feature h3{font-size:1.05rem; margin-bottom:.5rem}
|
||||||
|
.feature p{color:#2a2a2a; margin:0}
|
||||||
|
|
||||||
|
/* Showcases (copx) */
|
||||||
|
.copx{display:flex; gap:1.5rem; align-items:center; justify-content:center; padding:2rem 0 1rem}
|
||||||
|
.copx-moving{height:auto; width:min(420px,90vw); border-radius:18px}
|
||||||
|
.copx-moving2{height:auto; width:min(220px,60vw); border-radius:18px}
|
||||||
|
.copx-shadow-container{position:relative; animation:float 3s ease-in-out infinite}
|
||||||
|
.copx-shadow-container::before{
|
||||||
|
content:""; position:absolute; top:100%; left:50%; width:80%; height:16px;
|
||||||
|
background: radial-gradient(ellipse at center, rgba(0,0,0,.25), transparent); transform: translateX(-50%);
|
||||||
|
border-radius:50%; filter:blur(3px); z-index:-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copx-section{
|
||||||
|
display:grid; grid-template-columns: auto auto 1fr; gap:1.25rem; align-items:center;
|
||||||
|
border-radius:16px; padding:1.25rem; max-width:1100px; margin-inline:auto; overflow:hidden;
|
||||||
|
}
|
||||||
|
.copx-image img{ width:100%; max-width:200px; border-radius:14px }
|
||||||
|
.copx-description h2{ color:#818f08; font-size:clamp(1.2rem,2.4vw,1.6rem); margin-bottom:.35rem }
|
||||||
|
.copx-description p{ color:#050404; line-height:1.7 }
|
||||||
|
|
||||||
|
/* CTA */
|
||||||
|
.cta{
|
||||||
|
text-align:center; padding:2rem 1rem; background:linear-gradient(90deg,#cbe4e9,#f5ff69);
|
||||||
|
border-radius:20px; margin-top:3rem;
|
||||||
|
}
|
||||||
|
.cta a{
|
||||||
|
display:inline-block; background:#a85c04; color:#fff; padding:.8rem 1.3rem; border-radius:100px;
|
||||||
|
text-decoration:none; font-weight:700;
|
||||||
|
}
|
||||||
|
.cta a:hover{ background:#00c3ff }
|
||||||
|
|
||||||
|
/* Pricing */
|
||||||
|
.back-pricing{
|
||||||
|
background: linear-gradient(135deg, var(--gold), #494507, var(--gold));
|
||||||
|
border:2px solid rgba(255,215,0,.3);
|
||||||
|
box-shadow:0 12px 40px rgba(255,208,42,.28), inset 0 0 12px rgba(255,255,255,.18);
|
||||||
|
padding: clamp(1.2rem,2.5vw,2rem); margin: clamp(1.5rem,3vw,3rem) 0; border-radius: 32px; position:relative; overflow:hidden;
|
||||||
|
}
|
||||||
|
.back-pricing::after{
|
||||||
|
content:""; position:absolute; inset:-20% -20% auto auto; width:160%; height:160%;
|
||||||
|
background: radial-gradient(circle, rgba(255,231,133,.22), transparent 70%);
|
||||||
|
filter: blur(60px); z-index:0; pointer-events:none;
|
||||||
|
}
|
||||||
|
.product-mrkyai-pricing{
|
||||||
|
position:relative; z-index:1; color:#383400; text-align:center;
|
||||||
|
background: rgba(255,255,255,.15); backdrop-filter: blur(14px); border:1px solid rgba(255,255,255,.25);
|
||||||
|
border-radius: 28px; padding: clamp(1rem,2.5vw,2rem);
|
||||||
|
}
|
||||||
|
.pricing-title{ color:#fafbe6; margin-bottom:1.25rem }
|
||||||
|
.pricing-options{
|
||||||
|
display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:1rem; align-items:stretch;
|
||||||
|
}
|
||||||
|
.price-card{
|
||||||
|
background: linear-gradient(90deg,#e9e7cb,#f5ff69); border:2px solid #b1ae0a; border-radius:16px;
|
||||||
|
padding:1rem; transition: transform .25s ease, box-shadow .25s ease;
|
||||||
|
display:flex; flex-direction:column; justify-content:space-between;
|
||||||
|
}
|
||||||
|
.price-card:hover{ transform:translateY(-6px); box-shadow: 0 10px 24px rgba(25,135,84,.28) }
|
||||||
|
.pack-title{ font-size:1.1rem; margin:.25rem 0 .5rem; color:#3c3d00; font-weight:800 }
|
||||||
|
.justlist{ list-style:none; padding-left:0; margin:0 0 1rem; display:flex; flex-direction:column; gap:.55rem; text-align:left }
|
||||||
|
.justlist li{ position:relative; padding-left:28px; font-weight:500; color:#2e2e2e; line-height:1.45 }
|
||||||
|
.justlist li::before{ content:"✔"; position:absolute; left:0; top:0; color:#7c6f00; font-weight:900 }
|
||||||
|
.pack-price{ font-size:1.3rem; font-weight:900; color:#3c3d00; margin:.25rem 0 1rem }
|
||||||
|
.select-btn{ background:#198754; color:#fff; border:none; padding:.7rem 1rem; border-radius:10px; font-weight:700; }
|
||||||
|
.select-btn:hover{ background:#0f7a49 }
|
||||||
|
.recommended{ border-color:#ffd900; box-shadow:0 0 20px rgba(255,230,0,.28) }
|
||||||
|
.recommended::before{
|
||||||
|
content:"Rekomendasi"; position:absolute; top:-12px; left:50%; transform:translateX(-50%);
|
||||||
|
background:#fffb00; color:#000; font-size:.75rem; font-weight:800; padding:.25rem .6rem; border-radius:.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dynamic Products */
|
||||||
|
#dynamic-products{ padding-top:2rem }
|
||||||
|
.product-grid{
|
||||||
|
display:grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap:1rem;
|
||||||
|
}
|
||||||
|
.product-card{
|
||||||
|
background:#fff; border:1px solid rgba(0,0,0,.06); border-radius:14px; padding:1rem;
|
||||||
|
box-shadow:0 6px 20px rgba(0,0,0,.06);
|
||||||
|
display:flex; flex-direction:column; gap:.6rem;
|
||||||
|
}
|
||||||
|
.product-card img{ width:100%; height:180px; object-fit:cover; border-radius:10px }
|
||||||
|
.product-price{ font-weight:900; color:#3c3d00 }
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer{
|
||||||
|
background:#111; color:#fff; padding:2rem 0 2.5rem; margin-top:2rem;
|
||||||
|
border-top-left-radius: 20px; border-top-right-radius:20px;
|
||||||
|
}
|
||||||
|
footer a{ color:#a3d8ff; text-decoration:none }
|
||||||
|
footer a:hover{ text-decoration:underline }
|
||||||
|
|
||||||
|
/* Utilities */
|
||||||
|
.container-narrow{max-width:1200px;margin:auto;padding:0 16px}
|
||||||
|
.gradient-button a{ display:inline-block; background:linear-gradient(90deg,#7cab00,#b6d313); color:#fff; padding:.55rem .9rem; border-radius:.8rem; text-decoration:none; font-weight:800 }
|
||||||
|
.skeleton{
|
||||||
|
background:linear-gradient(90deg, #eee, #f7f7f7, #eee);
|
||||||
|
background-size:200% 100%; animation:shine 1.3s linear infinite; border-radius:12px;
|
||||||
|
}
|
||||||
|
@keyframes shine{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 1200px){
|
||||||
|
.feature-grid{ grid-template-columns: repeat(3, minmax(0,1fr)); }
|
||||||
|
.product-grid{ grid-template-columns: repeat(3, minmax(0,1fr)); }
|
||||||
|
.pricing-options{ grid-template-columns:repeat(3, minmax(0,1fr)); }
|
||||||
|
}
|
||||||
|
@media (max-width: 992px){
|
||||||
|
.hero{ grid-template-columns:1fr; gap:1.5rem; text-align:center }
|
||||||
|
.feature-grid{ grid-template-columns: repeat(2, minmax(0,1fr)) }
|
||||||
|
.product-grid{ grid-template-columns: repeat(2, minmax(0,1fr)) }
|
||||||
|
.pricing-options{ grid-template-columns:repeat(2, minmax(0,1fr)) }
|
||||||
|
.copx{ flex-direction:column }
|
||||||
|
.copx-section{ grid-template-columns: 1fr; text-align:center }
|
||||||
|
.nav-toggle{ display:inline-flex }
|
||||||
|
.nav-links{ position:absolute; left:0; right:0; top:64px; background:#fff; border-bottom:1px solid rgba(0,0,0,.06); flex-direction:column; padding: .75rem; display:none }
|
||||||
|
.nav-links.show{ display:flex }
|
||||||
|
}
|
||||||
|
@media (max-width: 576px){
|
||||||
|
.feature-grid{ grid-template-columns:1fr }
|
||||||
|
.product-grid{ grid-template-columns:1fr }
|
||||||
|
.pricing-options{ grid-template-columns:1fr }
|
||||||
|
.price-card{ padding:1rem }
|
||||||
|
.product-card img{ height:160px }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Respect reduced motion */
|
||||||
|
@media (prefers-reduced-motion: reduce){
|
||||||
|
.floating-image, .copx-shadow-container{ animation:none }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="container-narrow py-2">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<a href="index.html" class="nav-brand d-flex align-items-center gap-2 text-decoration-none">
|
||||||
|
<img src="assets/images/kediri-technopark-logo.png" alt="Kediri Technopark" />
|
||||||
|
<strong class="text-dark">Kedai Master</strong>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="nav-toggle" id="navToggle" aria-label="Buka menu">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul class="nav-links m-0" id="navLinks">
|
||||||
|
<li><a class="active" href="index.html">Home</a></li>
|
||||||
|
<li><a class="scroll-to-section" href="#services">Services</a></li>
|
||||||
|
<li><a class="scroll-to-section" href="#produk">Product</a></li>
|
||||||
|
<li><a class="scroll-to-section" href="#academy">Academy</a></li>
|
||||||
|
<li><a class="scroll-to-section" href="#about">About</a></li>
|
||||||
|
<li><a class="scroll-to-section" href="#contact">Contact</a></li>
|
||||||
|
<li><a id="usernameTop" class="fw-bold"></a></li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<div id="authArea" class="d-flex align-items-center">
|
||||||
|
<div class="gradient-button me-2" id="loginBtnWrapper">
|
||||||
|
<a id="modal_trigger" href="#modal"><i class="fa fa-sign-in-alt"></i> Sign In Now</a>
|
||||||
|
</div>
|
||||||
|
<div id="profileWrapper" class="profile-wrapper" style="display:none;">
|
||||||
|
<img id="profileImage" src="" alt="Profile" class="profile-image" />
|
||||||
|
<div id="tooltip" class="tooltip-box">
|
||||||
|
<div class="tooltip-content">
|
||||||
|
<p id="usernameTooltip" class="fw-bold mb-2"></p>
|
||||||
|
<button id="signOutBtn" class="signout-button">Sign Out</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="container-narrow">
|
||||||
|
|
||||||
|
<!-- Hero -->
|
||||||
|
<section class="hero" id="home">
|
||||||
|
<div class="hero-text">
|
||||||
|
<h1>Kedai Master</h1>
|
||||||
|
<p>Platform Point of Sale berbasis web untuk kebutuhan operasional kafe & restoran masa kini—fleksibel, interaktif, dan efisien.</p>
|
||||||
|
</div>
|
||||||
|
<div class="move-image">
|
||||||
|
<img src="assets/images/cupcoffekedmas.jpeg.jpg" alt="Mockup Aplikasi Kedai Master" class="floating-image" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Features -->
|
||||||
|
<section class="features" id="services">
|
||||||
|
<h2>Fitur Unggulan</h2>
|
||||||
|
<div class="feature-grid mt-3">
|
||||||
|
<div class="feature">
|
||||||
|
<h3>Manajemen Tenant & Kasir</h3>
|
||||||
|
<p>Tenant dapat mendaftar dan mengelola kafe serta akun kasir secara mandiri.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3>QR Pemesanan di Meja</h3>
|
||||||
|
<p>Tamu memesan langsung dari meja melalui QR code unik.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3>Otomatisasi Pesanan & Keuangan</h3>
|
||||||
|
<p>Transaksi tercatat otomatis, lengkap dengan laporan keuangan.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3>Request & Voting Lagu</h3>
|
||||||
|
<p>Tamu dapat memilih lagu untuk meningkatkan interaksi & suasana.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Motion Images -->
|
||||||
|
<section class="copx" aria-label="Demo Gambar Aplikasi">
|
||||||
|
<div class="copx-shadow-container">
|
||||||
|
<img src="assets/images/kemasmoving.gif" alt="Menu bergerak" class="copx-moving" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
<div class="copx-shadow-container">
|
||||||
|
<img src="assets/images/kedaimaster2.png" alt="Tampilan Kedai Master" class="copx-moving2" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Showcase -->
|
||||||
|
<section class="copx-section" id="about">
|
||||||
|
<div class="copx-image">
|
||||||
|
<img src="assets/images/kemasmenuimage.png" alt="Gambar menu 1" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
<div class="copx-image">
|
||||||
|
<img src="assets/images/kedaimaster1.png" alt="Gambar menu 2" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
<div class="copx-description">
|
||||||
|
<h2>Desain Menu Modern</h2>
|
||||||
|
<p>Tampilan menu bersih, interaktif, dan mudah dipahami pelanggan—memberi pengalaman visual yang memudahkan memilih hidangan favorit. Cocok untuk kafe & restoran masa kini.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<section class="cta" id="academy">
|
||||||
|
<h2 class="mb-3">Siap Tingkatkan Bisnis Anda?</h2>
|
||||||
|
<a href="contactkm.html" class="mt-2">Coba Kedai Master Sekarang</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Pricing -->
|
||||||
|
<section class="back-pricing" id="produk" aria-label="Paket Harga">
|
||||||
|
<div class="product-mrkyai-pricing">
|
||||||
|
<h2 class="pricing-title">OUR PACK KEDAI MASTER</h2>
|
||||||
|
<div class="pricing-options mt-3">
|
||||||
|
<article class="price-card">
|
||||||
|
<h3 class="pack-title">Starter Pack</h3>
|
||||||
|
<ul class="justlist">
|
||||||
|
<li>Manajemen menu & kategori</li>
|
||||||
|
<li>Dashboard penjualan dasar</li>
|
||||||
|
<li>1 user akses admin</li>
|
||||||
|
<li>Support via email</li>
|
||||||
|
</ul>
|
||||||
|
<div class="pack-price">Rp 249.000</div>
|
||||||
|
<button class="select-btn" onclick="window.location.href='contactkm.html'">Pilih Paket</button>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="price-card recommended">
|
||||||
|
<h3 class="pack-title">Professional Pack</h3>
|
||||||
|
<ul class="justlist">
|
||||||
|
<li>Semua fitur Starter</li>
|
||||||
|
<li>Integrasi kasir & printer thermal</li>
|
||||||
|
<li>AI pencatatan & prediksi stok</li>
|
||||||
|
<li>Notifikasi transaksi ke pelanggan</li>
|
||||||
|
<li>Support via WhatsApp</li>
|
||||||
|
</ul>
|
||||||
|
<div class="pack-price">Rp 499.000</div>
|
||||||
|
<button class="select-btn" onclick="window.location.href='contactkm.html'">Pilih Paket</button>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="price-card">
|
||||||
|
<h3 class="pack-title">Enterprise Pack</h3>
|
||||||
|
<ul class="justlist">
|
||||||
|
<li>Semua fitur Professional</li>
|
||||||
|
<li>AI analitik performa menu & outlet</li>
|
||||||
|
<li>Multi outlet & multi user</li>
|
||||||
|
<li>Integrasi loyalty customer</li>
|
||||||
|
<li>Priority support 24/7</li>
|
||||||
|
</ul>
|
||||||
|
<div class="pack-price">Rp 849.000</div>
|
||||||
|
<button class="select-btn" onclick="window.location.href='contactkm.html'">Pilih Paket</button>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Dynamic Products from API -->
|
||||||
|
<section class="features" id="dynamic-products" aria-live="polite" aria-busy="true">
|
||||||
|
<h2>Produk Lainnya</h2>
|
||||||
|
<div id="product-grid" class="product-grid mt-3">
|
||||||
|
<!-- Skeleton Loading -->
|
||||||
|
<div class="product-card">
|
||||||
|
<div class="skeleton" style="height:180px;"></div>
|
||||||
|
<div class="skeleton" style="height:18px; width:70%;"></div>
|
||||||
|
<div class="skeleton" style="height:14px; width:90%;"></div>
|
||||||
|
<div class="skeleton" style="height:20px; width:40%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="product-card">
|
||||||
|
<div class="skeleton" style="height:180px;"></div>
|
||||||
|
<div class="skeleton" style="height:18px; width:70%;"></div>
|
||||||
|
<div class="skeleton" style="height:14px; width:90%;"></div>
|
||||||
|
<div class="skeleton" style="height:20px; width:40%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="product-card">
|
||||||
|
<div class="skeleton" style="height:180px;"></div>
|
||||||
|
<div class="skeleton" style="height:18px; width:70%;"></div>
|
||||||
|
<div class="skeleton" style="height:14px; width:90%;"></div>
|
||||||
|
<div class="skeleton" style="height:20px; width:40%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="product-card">
|
||||||
|
<div class="skeleton" style="height:180px;"></div>
|
||||||
|
<div class="skeleton" style="height:18px; width:70%;"></div>
|
||||||
|
<div class="skeleton" style="height:14px; width:90%;"></div>
|
||||||
|
<div class="skeleton" style="height:20px; width:40%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p id="product-empty" class="text-center mt-3 d-none">Belum ada produk yang dapat ditampilkan.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Contact in Footer -->
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer id="contact">
|
||||||
|
<div class="container-narrow">
|
||||||
|
<div class="row justify-content-center g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<h4>Contact Us</h4>
|
||||||
|
<p style="color:#eee;">Sunan Giri GG. I No. 11, Rejomulyo, Kediri, Jawa Timur 64129</p>
|
||||||
|
<p><a href="tel:+6281318894994">0813 1889 4994</a></p>
|
||||||
|
<p><a href="mailto:marketing@kediritechnopark.com">marketing@kediritechnopark.com</a></p>
|
||||||
|
<p><a href="https://instagram.com/kediri.technopark" target="_blank" rel="noopener">@kediri.technopark</a></p>
|
||||||
|
<p><a href="https://kediritechnopark.com" target="_blank" rel="noopener">www.KEDIRITECHNOPARK.com</a></p>
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="https://wa.me/6281318894994" target="_blank" rel="noopener" style="margin-right:10px; font-size:20px;"><i class="fab fa-whatsapp"></i></a>
|
||||||
|
<a href="https://instagram.com/kediri.technopark" target="_blank" rel="noopener" style="font-size:20px;"><i class="fab fa-instagram"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6 text-center">
|
||||||
|
<h4>About Our Company</h4>
|
||||||
|
<div class="mb-3">
|
||||||
|
<img src="assets/images/logo-white.png" alt="Logo" style="height:46px" />
|
||||||
|
</div>
|
||||||
|
<p>Kediri Technopark adalah pusat pengembangan inovasi digital dan aplikasi untuk masyarakat dan pelaku usaha.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 text-center">
|
||||||
|
<div class="mt-2" style="opacity:.9">
|
||||||
|
<p>
|
||||||
|
© 2025 Kediri Technopark. All Rights Reserved.<br />
|
||||||
|
Design by <a href="https://templatemo.com/" target="_blank" rel="noopener">TemplateMo</a><br />
|
||||||
|
Distributed by <a href="https://themewagon.com/" target="_blank" rel="noopener">ThemeWagon</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script>
|
||||||
|
// Simple mobile nav toggle
|
||||||
|
const navToggle = document.getElementById('navToggle');
|
||||||
|
const navLinks = document.getElementById('navLinks');
|
||||||
|
if(navToggle && navLinks){
|
||||||
|
navToggle.addEventListener('click', () => {
|
||||||
|
navLinks.classList.toggle('show');
|
||||||
|
});
|
||||||
|
// Close menu on link click (mobile)
|
||||||
|
navLinks.querySelectorAll('a').forEach(a=>{
|
||||||
|
a.addEventListener('click', ()=> navLinks.classList.remove('show'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth UI (based on JWT in localStorage)
|
||||||
|
function parseJwt(token) {
|
||||||
|
try {
|
||||||
|
const base64Url = token.split('.')[1];
|
||||||
|
const base64 = decodeURIComponent(atob(base64Url).split('').map(c =>
|
||||||
|
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
|
||||||
|
return JSON.parse(base64);
|
||||||
|
} catch (e) { return null; }
|
||||||
|
}
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const loginBtnWrapper = document.getElementById("loginBtnWrapper");
|
||||||
|
const profileWrapper = document.getElementById("profileWrapper");
|
||||||
|
const profileImage = document.getElementById("profileImage");
|
||||||
|
const tooltip = document.getElementById("tooltip");
|
||||||
|
const signOutBtn = document.getElementById("signOutBtn");
|
||||||
|
const usernameTop = document.getElementById("usernameTop");
|
||||||
|
const usernameTooltip = document.getElementById("usernameTooltip");
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
const payload = parseJwt(token);
|
||||||
|
const username = payload?.username || "User";
|
||||||
|
|
||||||
|
if (loginBtnWrapper) loginBtnWrapper.style.display = "none";
|
||||||
|
if (profileWrapper) profileWrapper.style.display = "inline-block";
|
||||||
|
if (usernameTop) usernameTop.innerText = username;
|
||||||
|
if (usernameTooltip) usernameTooltip.innerText = username;
|
||||||
|
|
||||||
|
if (profileImage)
|
||||||
|
profileImage.src = "https://ui-avatars.com/api/?background=EEE&color=333&name=" + encodeURIComponent(username);
|
||||||
|
|
||||||
|
if (profileImage && tooltip) {
|
||||||
|
profileImage.addEventListener("click", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
tooltip.style.display = tooltip.style.display === "block" ? "none" : "block";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (tooltip && !tooltip.contains(e.target)) {
|
||||||
|
tooltip.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (signOutBtn) {
|
||||||
|
signOutBtn.addEventListener("click", function () {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (loginBtnWrapper) loginBtnWrapper.style.display = "inline-block";
|
||||||
|
if (profileWrapper) profileWrapper.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smooth scroll for internal anchors
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(anchor=>{
|
||||||
|
anchor.addEventListener('click', function(e){
|
||||||
|
const id = this.getAttribute('href').slice(1);
|
||||||
|
const target = document.getElementById(id);
|
||||||
|
if(target){
|
||||||
|
e.preventDefault();
|
||||||
|
target.scrollIntoView({behavior:'smooth', block:'start'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch dynamic products
|
||||||
|
(function loadProducts(){
|
||||||
|
const grid = document.getElementById('product-grid');
|
||||||
|
const empty = document.getElementById('product-empty');
|
||||||
|
if(!grid) return;
|
||||||
|
|
||||||
|
fetch("https://bot.kediritechnopark.com/webhook/store-production/products", {
|
||||||
|
headers: {
|
||||||
|
"accept": "*/*",
|
||||||
|
"content-type": "application/json"
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
mode: "cors",
|
||||||
|
credentials: "omit",
|
||||||
|
body: JSON.stringify({ type: "product" })
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(list => {
|
||||||
|
grid.innerHTML = ""; // clear skeleton
|
||||||
|
if(!Array.isArray(list) || list.length===0){
|
||||||
|
empty.classList.remove('d-none');
|
||||||
|
document.getElementById('dynamic-products').setAttribute('aria-busy','false');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let visible = 0;
|
||||||
|
list.forEach(product => {
|
||||||
|
if(product?.is_visible !== true) return;
|
||||||
|
visible++;
|
||||||
|
|
||||||
|
const card = document.createElement('article');
|
||||||
|
card.className = 'product-card';
|
||||||
|
|
||||||
|
const imgUrl = product.image && product.image.trim() !== "" ? product.image : 'assets/images/no-image.png';
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<img src="${imgUrl}" alt="${product.name ?? 'Produk'}" loading="lazy" />
|
||||||
|
<h3 class="h6 m-0">${product.name ?? ''}</h3>
|
||||||
|
<p class="m-0" style="color:#333">${product.description ?? ''}</p>
|
||||||
|
<div class="product-price mt-1">Rp ${Number(product.price||0).toLocaleString('id-ID')}</div>
|
||||||
|
${product.site_url ? `<a class="btn btn-sm btn-outline-success mt-2" href="${product.site_url}" target="_blank" rel="noopener">Lihat Detail</a>` : `<button class="btn btn-sm btn-success mt-2" onclick="window.location.href='contactkm.html'">Tanyakan Produk</button>`}
|
||||||
|
`;
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(visible===0){
|
||||||
|
empty.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
document.getElementById('dynamic-products').setAttribute('aria-busy','false');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Error fetching products:", err);
|
||||||
|
// keep skeleton but show message
|
||||||
|
const msg = document.createElement('p');
|
||||||
|
msg.className="text-center mt-3";
|
||||||
|
msg.textContent = "Gagal memuat produk. Silakan refresh halaman.";
|
||||||
|
grid.after(msg);
|
||||||
|
document.getElementById('dynamic-products').setAttribute('aria-busy','false');
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Bootstrap Bundle (for potential components you might add later) -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
304
src/App.js
@@ -2,60 +2,69 @@ import React, { useEffect, useState, useRef } from 'react';
|
|||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
import styles from './components/Styles.module.css';
|
import styles from './components/Styles.module.css';
|
||||||
|
|
||||||
// Import components
|
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
|
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import HeroSection from './components/HeroSection';
|
import HeroSection from './components/HeroSection';
|
||||||
import ServicesSection from './components/ServicesSection';
|
import ServicesSection from './components/ServicesSection';
|
||||||
import ProductSection from './components/ProductSection';
|
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
|
||||||
import KnowledgeBaseSection from './components/KnowledgeBaseSection';
|
import KnowledgeBaseSection from './components/KnowledgeBaseSection';
|
||||||
|
// ClientsSection hidden temporarily
|
||||||
import ClientsSection from './components/ClientsSection';
|
import ClientsSection from './components/ClientsSection';
|
||||||
|
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';
|
||||||
|
|
||||||
import Dashboard from './components/Dashboard';
|
import Dashboard from './components/Dashboard';
|
||||||
|
import CreateProductPage from './components/CreateProductPage';
|
||||||
import ProductsPage from './components/pages/ProductsPage';
|
import ProductsPage from './components/pages/ProductsPage';
|
||||||
|
|
||||||
|
import processProducts from './helper/processProducts';
|
||||||
|
|
||||||
function HomePage({
|
function HomePage({
|
||||||
hoveredCard,
|
|
||||||
setHoveredCard,
|
|
||||||
selectedProduct,
|
|
||||||
setSelectedProduct,
|
setSelectedProduct,
|
||||||
showedModal,
|
|
||||||
setShowedModal,
|
setShowedModal,
|
||||||
productSectionRef,
|
productSectionRef,
|
||||||
courseSectionRef
|
courseSectionRef,
|
||||||
|
scrollToProduct,
|
||||||
|
scrollToCourse,
|
||||||
|
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 />
|
||||||
<ServicesSection />
|
<ServicesSection />
|
||||||
<ProductSection
|
<ProductSection
|
||||||
productSectionRef={productSectionRef}
|
productSectionRef={productSectionRef}
|
||||||
hoveredCard={hoveredCard}
|
|
||||||
setHoveredCard={setHoveredCard}
|
|
||||||
setSelectedProduct={setSelectedProduct}
|
setSelectedProduct={setSelectedProduct}
|
||||||
setShowedModal={setShowedModal}
|
setShowedModal={setShowedModal}
|
||||||
|
setWillDo={setWillDo}
|
||||||
/>
|
/>
|
||||||
<AcademySection
|
<AcademySection
|
||||||
courseSectionRef={courseSectionRef}
|
courseSectionRef={courseSectionRef}
|
||||||
hoveredCard={hoveredCard}
|
|
||||||
setHoveredCard={setHoveredCard}
|
|
||||||
setSelectedProduct={setSelectedProduct}
|
setSelectedProduct={setSelectedProduct}
|
||||||
setShowedModal={setShowedModal} />
|
setShowedModal={setShowedModal}
|
||||||
<AboutUsSection />
|
setWillDo={setWillDo}
|
||||||
|
/>
|
||||||
<KnowledgeBaseSection />
|
<KnowledgeBaseSection />
|
||||||
<ClientsSection />
|
<ClientsSection />
|
||||||
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJwt(token) {
|
function parseJwt(token) {
|
||||||
try {
|
try {
|
||||||
const base64Url = token.split('.')[1];
|
const base64Url = token.split('.')[1];
|
||||||
@@ -74,60 +83,196 @@ function parseJwt(token) {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// State yang diperlukan untuk HomePage
|
|
||||||
const [hoveredCard, setHoveredCard] = useState(null);
|
|
||||||
|
|
||||||
const [subscriptions, setSubscriptions] = useState(null);
|
const [subscriptions, setSubscriptions] = useState(null);
|
||||||
const [selectedProduct, setSelectedProduct] = useState({});
|
const [selectedProduct, setSelectedProduct] = useState({});
|
||||||
const [showedModal, setShowedModal] = useState(null); // 'product' | 'login' | null
|
const [showedModal, setShowedModal] = useState(null);
|
||||||
const [postLoginAction, setPostLoginAction] = useState(null);
|
const [subProductOf, setSubProductOf] = useState(null);
|
||||||
|
|
||||||
const [username, setUsername] = useState(null);
|
const [username, setUsername] = useState(null);
|
||||||
|
|
||||||
const productSectionRef = useRef(null);
|
const productSectionRef = useRef(null);
|
||||||
const courseSectionRef = useRef(null);
|
const courseSectionRef = useRef(null);
|
||||||
|
|
||||||
|
const [productModalRequest, setProductModalRequest] = useState(null);
|
||||||
|
const [willDo, setWillDo] = useState('');
|
||||||
|
|
||||||
|
|
||||||
|
const scrollToProduct = () => {
|
||||||
|
productSectionRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToCourse = () => {
|
||||||
|
courseSectionRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestLogin = (nextAction) => {
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set('next', nextAction);
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
setShowedModal('login');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ambil token dan user info dari cookie
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Ambil token dari cookies
|
|
||||||
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
||||||
if (match) {
|
if (match) {
|
||||||
const token = match[2];
|
const token = match[2];
|
||||||
|
fetch('https://bot.kediritechnopark.com/webhook/user-production/data', {
|
||||||
fetch('https://bot.kediritechnopark.com/webhook/user-dev/data', {
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer ' + token
|
'Authorization': 'Bearer ' + token
|
||||||
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|
||||||
if (data && data.token) {
|
if (data && data.token) {
|
||||||
// Update token with data[0].token
|
|
||||||
document.cookie = `token=${data.token}; path=/`;
|
document.cookie = `token=${data.token}; path=/`;
|
||||||
console.log(data)
|
setSubscriptions(data.subscriptions);
|
||||||
setSubscriptions(data.subscriptions)
|
|
||||||
const payload = parseJwt(data.token);
|
const payload = parseJwt(data.token);
|
||||||
if (payload && payload.username) {
|
if (payload && payload.username) {
|
||||||
setUsername(payload.username);
|
setUsername(payload.username);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn('Token tidak ditemukan dalam data.');
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Fetch error:', err));
|
.catch(err => {
|
||||||
|
document.cookie = 'token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
|
||||||
|
setUsername(null);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
const scrollToProduct = () => {
|
|
||||||
productSectionRef.current?.scrollIntoView({ behavior: "smooth" });
|
useEffect(() => {
|
||||||
};
|
const params = new URLSearchParams(window.location.search);
|
||||||
const scrollToCourse = () => {
|
const modalType = params.get('modal');
|
||||||
courseSectionRef.current?.scrollIntoView({ behavior: "smooth" });
|
const tab = params.get('tab');
|
||||||
};
|
const productId = params.get('product_id');
|
||||||
|
const authorizedUri = params.get('authorized_uri');
|
||||||
|
const unauthorizedUri = params.get('unauthorized_uri');
|
||||||
|
|
||||||
|
if (modalType === 'product' && productId) {
|
||||||
|
const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2];
|
||||||
|
|
||||||
|
// Simpan semua param penting ke localStorage
|
||||||
|
localStorage.setItem('product_id', productId);
|
||||||
|
if (authorizedUri) localStorage.setItem('authorized_uri', authorizedUri);
|
||||||
|
if (unauthorizedUri) localStorage.setItem('unauthorized_uri', unauthorizedUri);
|
||||||
|
|
||||||
|
// Jika belum login, tampilkan modal login
|
||||||
|
if (!token && authorizedUri) {
|
||||||
|
setShowedModal('login');
|
||||||
|
}
|
||||||
|
// Jika sudah login, tidak langsung fetch di sini — akan diproses saat subscriptions tersedia
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab === 'products') scrollToProduct();
|
||||||
|
if (tab === 'academy') scrollToCourse();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const modalType = params.get('modal');
|
||||||
|
const productId = parseInt(params.get('product_id'));
|
||||||
|
const authorizedUri = params.get('authorized_uri');
|
||||||
|
const unauthorizedUri = params.get('unauthorized_uri');
|
||||||
|
|
||||||
|
const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2];
|
||||||
|
|
||||||
|
if (modalType === 'product' && productId) {
|
||||||
|
if (!token && authorizedUri) {
|
||||||
|
setShowedModal('login'); // belum login → tampilkan login modal
|
||||||
|
} else {
|
||||||
|
// sudah login → lanjutkan proses otorisasi saat subscriptions tersedia
|
||||||
|
setProductModalRequest({ productId, authorizedUri, unauthorizedUri });
|
||||||
|
console.log('modal')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(subscriptions)
|
||||||
|
if (!productModalRequest || !subscriptions) return;
|
||||||
|
|
||||||
|
const { productId, authorizedUri, unauthorizedUri } = productModalRequest;
|
||||||
|
|
||||||
|
const hasAccess = subscriptions && subscriptions.some(
|
||||||
|
sub => sub.product_id === productId || sub.product_parent_id === productId
|
||||||
|
);
|
||||||
|
console.log("hasAccess:", hasAccess);
|
||||||
|
|
||||||
|
if (hasAccess) {
|
||||||
|
if (authorizedUri) {
|
||||||
|
let finalUri = decodeURIComponent(authorizedUri);
|
||||||
|
const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2];
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = finalUri;
|
||||||
|
} else {
|
||||||
|
// fallback ambil detail produk via fetch
|
||||||
|
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
itemsId: [productId],
|
||||||
|
withChildren: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
const processed = processProducts(data);
|
||||||
|
setSelectedProduct(processed[0]);
|
||||||
|
setShowedModal('product');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Fetch product error:', err));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (unauthorizedUri) {
|
||||||
|
window.location.href = decodeURIComponent(unauthorizedUri);
|
||||||
|
} else {
|
||||||
|
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
itemsId: [productId],
|
||||||
|
withChildren: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
const processed = processProducts(data);
|
||||||
|
setSelectedProduct(processed[0]);
|
||||||
|
setShowedModal('product');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Fetch product error:', err));
|
||||||
|
|
||||||
|
console.log('modal')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setProductModalRequest(null); // reset
|
||||||
|
}, [subscriptions, productModalRequest]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -137,13 +282,8 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
// Hapus cookie token dengan mengatur tanggal kadaluarsa ke masa lalu
|
|
||||||
document.cookie = 'token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
|
document.cookie = 'token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
|
||||||
|
setUsername(null);
|
||||||
// Jika kamu menggunakan state seperti `setUsername`, bersihkan di sini juga
|
|
||||||
setUsername(null); // jika applicable
|
|
||||||
|
|
||||||
// Redirect ke homepage atau reload halaman
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,64 +305,82 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Header username={username} scrollToProduct={scrollToProduct} scrollToCourse={scrollToCourse} setShowedModal={setShowedModal} handleLogout={handleLogout} />
|
<Header
|
||||||
|
username={username}
|
||||||
|
scrollToProduct={scrollToProduct}
|
||||||
|
scrollToCourse={scrollToCourse}
|
||||||
|
setShowedModal={setShowedModal}
|
||||||
|
handleLogout={handleLogout}
|
||||||
|
/>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
<HomePage
|
<HomePage
|
||||||
hoveredCard={hoveredCard}
|
|
||||||
setHoveredCard={setHoveredCard}
|
|
||||||
selectedProduct={selectedProduct}
|
|
||||||
setSelectedProduct={setSelectedProduct}
|
setSelectedProduct={setSelectedProduct}
|
||||||
showedModal={showedModal}
|
|
||||||
setShowedModal={setShowedModal}
|
setShowedModal={setShowedModal}
|
||||||
productSectionRef={productSectionRef}
|
productSectionRef={productSectionRef}
|
||||||
courseSectionRef={courseSectionRef}
|
courseSectionRef={courseSectionRef}
|
||||||
|
setWillDo={setWillDo}
|
||||||
|
scrollToProduct={scrollToProduct}
|
||||||
|
scrollToCourse={scrollToCourse}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route path="/dashboard" element={<ProductsPage
|
||||||
|
setShowedModal={setShowedModal}
|
||||||
|
setSelectedProduct={setSelectedProduct} subscriptions={subscriptions} setWillDo={setWillDo} />} />
|
||||||
<Route
|
<Route
|
||||||
path="/products"
|
path="/admin"
|
||||||
element={
|
element={
|
||||||
<ProductsPage subscriptions={subscriptions}/>
|
<Dashboard
|
||||||
}
|
setShowedModal={(e, productId) => {
|
||||||
|
setShowedModal(e);
|
||||||
|
setSubProductOf(productId);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="/dashboard"
|
|
||||||
element={
|
|
||||||
<Dashboard />
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
<Footer />
|
|
||||||
{/* Unified Modal */}
|
{/* Modal */}
|
||||||
{showedModal && (
|
{showedModal && (
|
||||||
<div
|
<div
|
||||||
className={styles.modal}
|
className={styles.modal}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.delete('modal');
|
||||||
|
url.searchParams.delete('product_id');
|
||||||
|
url.searchParams.delete('authorized_uri');
|
||||||
|
url.searchParams.delete('unauthorized_uri');
|
||||||
|
url.searchParams.delete('next');
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
setShowedModal(null);
|
setShowedModal(null);
|
||||||
setSelectedProduct({});
|
setSelectedProduct({});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className={styles.modalBody} onClick={(e) => e.stopPropagation()}>
|
||||||
className={styles.modalBody}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{showedModal === 'product' && (
|
{showedModal === 'product' && (
|
||||||
<ProductDetailPage
|
<ProductDetailPage
|
||||||
subscriptions={subscriptions}
|
subscriptions={subscriptions}
|
||||||
setPostLoginAction={setPostLoginAction}
|
requestLogin={requestLogin}
|
||||||
setShowedModal={setShowedModal}
|
|
||||||
product={selectedProduct}
|
product={selectedProduct}
|
||||||
onClose={() => {
|
setShowedModal={setShowedModal}
|
||||||
setShowedModal(null);
|
willDo={willDo}
|
||||||
setSelectedProduct({});
|
setWillDo={setWillDo}
|
||||||
}}
|
/>
|
||||||
|
)}
|
||||||
|
{showedModal === 'create-item' && (
|
||||||
|
<CreateProductPage
|
||||||
|
parentId={subProductOf}
|
||||||
|
subscriptions={subscriptions}
|
||||||
|
requestLogin={requestLogin}
|
||||||
|
product={selectedProduct}
|
||||||
|
setShowedModal={setShowedModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showedModal === 'login' && (
|
{showedModal === 'login' && (
|
||||||
<Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} />
|
<Login setShowedModal={setShowedModal} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,38 +1,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Container, Row, Col, Button } from 'react-bootstrap';
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
|
import styles from './AboutUsSection.module.css';
|
||||||
|
import { CheckCircle } from 'lucide-react';
|
||||||
|
import AnimatedBackground from './AnimatedBackground'; // Impor komponen baru
|
||||||
|
import shared from './Styles.module.css';
|
||||||
|
import useInView from '../hooks/useInView';
|
||||||
|
|
||||||
const AboutUsSection = () => {
|
const AboutUsSection = () => {
|
||||||
|
const { ref, inView } = useInView();
|
||||||
return (
|
return (
|
||||||
<section id="about" className="about-us section pt-5">
|
<section id="about" ref={ref} style={{scrollMarginTop: '65px'}} className={`${styles.aboutSection} ${shared.revealSection} ${inView ? shared.isVisible : ''}`}>
|
||||||
|
<AnimatedBackground /> {/* Komponen animasi sebagai latar belakang */}
|
||||||
|
<div className={styles.contentWrapper}>
|
||||||
<Container>
|
<Container>
|
||||||
<Row className="align-items-center">
|
<Row className="justify-content-center">
|
||||||
<Col lg={6}>
|
<Col lg={8}>
|
||||||
<div className="section-heading">
|
<div className={styles.textContent}>
|
||||||
<span style={{ color: '#6a59ff', fontWeight: 'bold' }}>Kediri Technopark</span>
|
<h2 className={styles.sectionTitle}>Tentang Kami</h2>
|
||||||
<h2 className="mt-2">ABOUT US</h2>
|
<p className={styles.paragraph}>
|
||||||
<img src="/assets/images/heading-line-dec.png" alt="" />
|
Kediri Technopark adalah ekosistem inovasi yang didedikasikan untuk mendorong pertumbuhan talenta digital dan akselerasi bisnis teknologi. Kami menyediakan fasilitas, program, dan jaringan yang dibutuhkan untuk mengubah ide brilian menjadi solusi nyata yang berdampak.
|
||||||
<p className="mt-3">
|
|
||||||
<strong>Kediri Technopark: Katalis Inovasi dan Pusat Pertumbuhan Digital Lokal</strong><br /><br />
|
|
||||||
Kediri Technopark adalah inisiatif strategis yang bertujuan membangun ekosistem teknologi dan inovasi yang dinamis di Kediri, Jawa Timur. Kami menyediakan infrastruktur, sumber daya, dan komunitas pendukung yang dibutuhkan untuk mendorong pertumbuhan startup dan bisnis IT yang sudah ada.<br /><br />
|
|
||||||
Dengan misi memberdayakan talenta lokal, menjembatani teknologi dan industri, serta mempercepat transformasi digital, Kediri Technopark berkomitmen menjadi penggerak kemajuan ekonomi dan teknologi, baik di tingkat lokal maupun nasional.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 d-flex gap-3">
|
<ul className={styles.valueList}>
|
||||||
<Button href="konsultasi.html" className="px-4 py-2 rounded-pill text-white" style={{ background: 'linear-gradient(to right, #6a59ff, #8261ee)', border: 'none' }}>
|
<li><CheckCircle size={20} className={styles.listIcon} /><span>Inovasi Berkelanjutan</span></li>
|
||||||
Konsultasi
|
<li><CheckCircle size={20} className={styles.listIcon} /><span>Kolaborasi Komunitas</span></li>
|
||||||
</Button>
|
<li><CheckCircle size={20} className={styles.listIcon} /><span>Pemberdayaan Talenta</span></li>
|
||||||
<Button href="https://wa.me/6281318894994" target="_blank" variant="outline-success" className="px-4 py-2 rounded-pill">
|
</ul>
|
||||||
<i className="fab fa-whatsapp"></i> WhatsApp
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col lg={6}>
|
|
||||||
<div className="right-image">
|
|
||||||
<img src="/assets/images/about-right-dec.png" alt="" className="img-fluid" />
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
60
src/components/AboutUsSection.module.css
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
.aboutSection {
|
||||||
|
padding: 80px 0; /* Ditinggikan dari 60px */
|
||||||
|
position: relative;
|
||||||
|
background-color: #0f172a; /* Latar belakang biru gelap (slate-900) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menghapus .blueprintGrid */
|
||||||
|
|
||||||
|
.contentWrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textContent {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: clamp(1.8rem, 4vw, 2.2rem);
|
||||||
|
color: #ffffff; /* Warna teks putih */
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #cbd5e1; /* Warna teks abu-abu terang (slate-300) */
|
||||||
|
margin-bottom: 24px;
|
||||||
|
max-width: 700px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueList {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueList li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: #f1f5f9; /* Warna teks putih keabuan (slate-100) */
|
||||||
|
}
|
||||||
|
|
||||||
|
.listIcon {
|
||||||
|
color: #38bdf8; /* Warna ikon biru cerah (sky-400) */
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
@@ -1,54 +1,93 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Container } from 'react-bootstrap';
|
import { Container } from 'react-bootstrap';
|
||||||
import styles from './Styles.module.css';
|
import styles from './Styles.module.css';
|
||||||
|
import useInView from '../hooks/useInView';
|
||||||
|
|
||||||
const AcademySection = ({hoveredCard, setHoveredCard, setSelectedProduct, setShowedModal, courseSectionRef}) => {
|
const AcademySection = ({setSelectedProduct, setShowedModal, courseSectionRef, setWillDo}) => {
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
|
const [hoveredCard, setHoveredCard] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
|
// Fetch all items to compute module/sessions reliably, then filter courses client-side
|
||||||
|
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({ type: 'course' }),
|
body: JSON.stringify({}),
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => setProducts(data))
|
.then(data => {
|
||||||
|
const all = Array.isArray(data) ? data : [];
|
||||||
|
const moduleCountMap = {};
|
||||||
|
const sessionsCountMap = {};
|
||||||
|
all.forEach(item => {
|
||||||
|
const parentId = item?.sub_product_of;
|
||||||
|
if (parentId) {
|
||||||
|
moduleCountMap[parentId] = (moduleCountMap[parentId] || 0) + 1;
|
||||||
|
const s = Number(item?.sessions || item?.session_count || 0);
|
||||||
|
sessionsCountMap[parentId] = (sessionsCountMap[parentId] || 0) + (isNaN(s) ? 0 : s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const coursesOnly = all.filter(p => (p?.type || '').toLowerCase() === 'course');
|
||||||
|
const enriched = coursesOnly.map(p => ({
|
||||||
|
...p,
|
||||||
|
module_count: p?.module_count ?? p?.modules ?? moduleCountMap[p.id] ?? 0,
|
||||||
|
session_count: p?.session_count ?? p?.sessions ?? sessionsCountMap[p.id] ?? 0,
|
||||||
|
}));
|
||||||
|
setProducts(enriched);
|
||||||
|
})
|
||||||
.catch(err => console.error('Fetch error:', err));
|
.catch(err => console.error('Fetch error:', err));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const { ref, inView } = useInView();
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<section id="services" className="services pt-5">
|
<section id="academy" style={{scrollMarginTop: '65px' }} className={`services pt-5 ${styles.academySection} ${styles.revealSection} ${inView ? styles.isVisible : ''}`} ref={(el) => {
|
||||||
|
if (typeof courseSectionRef === 'function') courseSectionRef(el);
|
||||||
|
if (ref) ref.current = el;
|
||||||
|
}}>
|
||||||
<Container>
|
<Container>
|
||||||
<div className="section-heading text-center mb-4">
|
<div className={styles.sectionHeader}>
|
||||||
<h4>OUR <em>ACADEMY PROGRAM</em></h4>
|
<div className={styles.sectionEyebrow}>Academy</div>
|
||||||
<img src="/assets/images/heading-line-dec.png" alt="" />
|
<h2 className={styles.sectionTitle}>Our Academy Program</h2>
|
||||||
<p>Academy Program adalah wadah belajar digital untuk anak-anak dan remaja. Didesain interaktif, kreatif, dan gratis — setiap modul membekali peserta dengan keterampilan masa depan, dari teknologi dasar hingga coding dan proyek nyata.</p>
|
<div className={styles.sectionRule} />
|
||||||
|
<p className={styles.sectionSubtitle}>
|
||||||
|
Academy Program adalah wadah belajar digital untuk anak-anak dan remaja. Didesain interaktif, kreatif, dan gratis — setiap modul membekali peserta dengan keterampilan masa depan, dari teknologi dasar hingga coding dan proyek nyata.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.coursesGrid}>
|
<div className={styles.coursesGrid}>
|
||||||
{products &&
|
{products &&
|
||||||
products[0]?.name &&
|
products[0]?.name &&
|
||||||
products.map(product => (
|
products
|
||||||
|
.map(product => (
|
||||||
<div
|
<div
|
||||||
key={product.id}
|
key={product.id}
|
||||||
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
|
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
|
||||||
onClick={() => {
|
|
||||||
setSelectedProduct(product);
|
|
||||||
setShowedModal('product');
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setHoveredCard(product.id)}
|
onMouseEnter={() => setHoveredCard(product.id)}
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
|
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
|
||||||
{product.price === 0 && (
|
{product.price === 0 && (
|
||||||
<span className={styles.courseLabel}>Free</span>
|
<span className={styles.courseLabel}>Free</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.courseContent}>
|
<div className={styles.courseContentTop}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
<h3 className={styles.courseTitle}>{product.name}</h3>
|
<h3 className={styles.courseTitle}>{product.name}</h3>
|
||||||
|
<div className={styles.pillRow}>
|
||||||
|
<span className={`${styles.pill} ${styles.pillModules}`}>{Number(product?.module_count || 0)} Modul</span>
|
||||||
|
<span className={`${styles.pill} ${styles.pillSessions}`}>{Number(product?.session_count || 0)} Sesi</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.titleSeparator} />
|
||||||
<p className={styles.courseDesc}>{product.description}</p>
|
<p className={styles.courseDesc}>{product.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.courseContentBottom}>
|
||||||
<div className={styles.coursePrice}>
|
<div className={styles.coursePrice}>
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
@@ -62,6 +101,12 @@ const AcademySection = ({hoveredCard, setHoveredCard, setSelectedProduct, setSho
|
|||||||
: `Rp ${product.price.toLocaleString('id-ID')}`}
|
: `Rp ${product.price.toLocaleString('id-ID')}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button className={`${styles.enrollButton}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedProduct(product);
|
||||||
|
setShowedModal('product');
|
||||||
|
setWillDo('checkout');
|
||||||
|
}}>Daftar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
161
src/components/AnimatedBackground.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import styles from './AnimatedBackground.module.css';
|
||||||
|
|
||||||
|
const AnimatedBackground = () => {
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
let animationFrameId;
|
||||||
|
let particles = [];
|
||||||
|
|
||||||
|
// Determine particle count based on screen size
|
||||||
|
const getParticleCount = () => {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
if (width <= 400) return 30; // Very small screens
|
||||||
|
if (width <= 576) return 40; // Small screens
|
||||||
|
if (width <= 768) return 50; // Medium screens
|
||||||
|
return 70; // Large screens
|
||||||
|
};
|
||||||
|
|
||||||
|
function Particle(x, y, vx, vy) {
|
||||||
|
this.x = x; this.y = y; this.vx = vx; this.vy = vy;
|
||||||
|
|
||||||
|
// Adjust particle radius based on screen size
|
||||||
|
if (window.innerWidth <= 400) {
|
||||||
|
this.radius = 1.0; // Smaller radius for very small screens
|
||||||
|
} else if (window.innerWidth <= 576) {
|
||||||
|
this.radius = 1.2; // Medium radius for small screens
|
||||||
|
} else {
|
||||||
|
this.radius = 1.5; // Default radius for larger screens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupCanvas = () => {
|
||||||
|
const parent = canvas.parentElement;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const rect = parent.getBoundingClientRect();
|
||||||
|
const cssWidth = parent.clientWidth;
|
||||||
|
const cssHeight = parent.clientHeight;
|
||||||
|
|
||||||
|
canvas.width = cssWidth * dpr;
|
||||||
|
canvas.height = cssHeight * dpr;
|
||||||
|
canvas.style.width = `${cssWidth}px`;
|
||||||
|
canvas.style.height = `${cssHeight}px`;
|
||||||
|
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
// Get particle count based on current screen size
|
||||||
|
const particleCount = getParticleCount();
|
||||||
|
|
||||||
|
particles = [];
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
particles.push(new Particle(Math.random() * cssWidth, Math.random() * cssHeight, (Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.5));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectParticles() {
|
||||||
|
const cssWidth = canvas.clientWidth;
|
||||||
|
const cssHeight = canvas.clientHeight;
|
||||||
|
|
||||||
|
// Adjust connection distance based on screen size
|
||||||
|
let connectionDistance = 90;
|
||||||
|
if (window.innerWidth <= 400) {
|
||||||
|
connectionDistance = 60; // Smaller connection distance for very small screens
|
||||||
|
} else if (window.innerWidth <= 576) {
|
||||||
|
connectionDistance = 70; // Medium connection distance for small screens
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < particles.length; i++) {
|
||||||
|
for (let j = i + 1; j < particles.length; j++) {
|
||||||
|
const p1 = particles[i];
|
||||||
|
const p2 = particles[j];
|
||||||
|
|
||||||
|
// Array posisi p2, termasuk "hantu" di 8 sisi lain
|
||||||
|
const p2Positions = [
|
||||||
|
{ x: p2.x, y: p2.y }, // Posisi asli
|
||||||
|
{ x: p2.x + cssWidth, y: p2.y }, // Kanan
|
||||||
|
{ x: p2.x - cssWidth, y: p2.y }, // Kiri
|
||||||
|
{ x: p2.x, y: p2.y + cssHeight }, // Bawah
|
||||||
|
{ x: p2.x, y: p2.y - cssHeight }, // Atas
|
||||||
|
{ x: p2.x + cssWidth, y: p2.y + cssHeight }, // Kanan-bawah
|
||||||
|
{ x: p2.x - cssWidth, y: p2.y - cssHeight }, // Kiri-atas
|
||||||
|
{ x: p2.x + cssWidth, y: p2.y - cssHeight }, // Kanan-atas
|
||||||
|
{ x: p2.x - cssWidth, y: p2.y + cssHeight } // Kiri-bawah
|
||||||
|
];
|
||||||
|
|
||||||
|
// Cari jarak terpendek ke p2 atau salah satu hantunya
|
||||||
|
for (const pos of p2Positions) {
|
||||||
|
const distance = Math.sqrt((p1.x - pos.x) ** 2 + (p1.y - pos.y) ** 2);
|
||||||
|
|
||||||
|
if (distance < connectionDistance) {
|
||||||
|
ctx.strokeStyle = `rgba(255, 255, 255, ${1 - distance / connectionDistance})`;
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(p1.x, p1.y);
|
||||||
|
ctx.lineTo(pos.x, pos.y); // Gambar garis ke posisi terdekat (bisa jadi hantu)
|
||||||
|
ctx.stroke();
|
||||||
|
break; // Hanya gambar satu garis terpendek
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
const cssWidth = canvas.clientWidth;
|
||||||
|
const cssHeight = canvas.clientHeight;
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Adjust animation speed based on screen size
|
||||||
|
let speedFactor = 1.0;
|
||||||
|
if (window.innerWidth <= 400) {
|
||||||
|
speedFactor = 0.7; // Slower animation for very small screens
|
||||||
|
} else if (window.innerWidth <= 576) {
|
||||||
|
speedFactor = 0.8; // Slightly slower animation for small screens
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of particles) {
|
||||||
|
// LOGIKA BARU: Partikel tembus (wrapping) bukan memantul (bounce)
|
||||||
|
if (p.x > cssWidth + p.radius) p.x = -p.radius;
|
||||||
|
else if (p.x < -p.radius) p.x = cssWidth + p.radius;
|
||||||
|
|
||||||
|
if (p.y > cssHeight + p.radius) p.y = -p.radius;
|
||||||
|
else if (p.y < -p.radius) p.y = cssHeight + p.radius;
|
||||||
|
|
||||||
|
p.x += p.vx * speedFactor;
|
||||||
|
p.y += p.vy * speedFactor;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectParticles();
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCanvas();
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Handle resize events to adjust particle count
|
||||||
|
const handleResize = () => {
|
||||||
|
setupCanvas();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <canvas ref={canvasRef} className={styles.particleCanvas}></canvas>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnimatedBackground;
|
||||||
7
src/components/AnimatedBackground.module.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.particleCanvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 0;
|
||||||
|
/* Ukuran akan diatur oleh JavaScript */
|
||||||
|
}
|
||||||
@@ -1,23 +1,38 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Container, Row, Col, Image } from 'react-bootstrap';
|
import { Container, Row, Col, Image } from 'react-bootstrap';
|
||||||
|
import styles from './Styles.module.css';
|
||||||
|
import useInView from '../hooks/useInView';
|
||||||
|
|
||||||
const ClientsSection = () => {
|
const ClientsSection = () => {
|
||||||
|
const logos = [
|
||||||
|
'logo_pemprov_jatim.png',
|
||||||
|
'suar.avif',
|
||||||
|
'kloowear.webp',
|
||||||
|
'psi.png',
|
||||||
|
'cafe-hore.png'
|
||||||
|
];
|
||||||
|
|
||||||
|
const { ref, inView } = useInView();
|
||||||
return (
|
return (
|
||||||
<section id="clients" className="the-clients section pt-5">
|
<section id="clients" ref={ref} className={`the-clients section py-5 ${styles.revealSection} ${inView ? styles.isVisible : ''}`}>
|
||||||
<Container>
|
<Container>
|
||||||
<Row>
|
<Row>
|
||||||
<Col lg={{ span: 8, offset: 2 }}>
|
<Col>
|
||||||
<div className="section-heading text-center mb-4">
|
<div className="section-heading mb-4">
|
||||||
<h4>TRUSTED BY <em>OUR CLIENTS</em></h4>
|
<h4>TRUSTED BY <em>OUR CLIENTS</em></h4>
|
||||||
<img src="/assets/images/heading-line-dec.png" alt="" className="mb-3" />
|
|
||||||
<p>We are proud to work with these amazing brands and organizations.</p>
|
<p>We are proud to work with these amazing brands and organizations.</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="clients-carousel" className="d-flex justify-content-center flex-wrap">
|
<div id="clients-carousel" className="d-flex justify-content-left flex-wrap">
|
||||||
{[1, 2, 3, 4, 5].map((num) => (
|
{logos.map((logo, index) => (
|
||||||
<div key={num} className="client-logo-wrapper m-2">
|
<div className={`${styles.clientLogoWrapper} m-2`} key={index}>
|
||||||
<Image src={`/assets/images/client-logo${num}.png`} alt={`Client ${num}`} fluid />
|
<Image
|
||||||
|
src={`/assets/${logo}`}
|
||||||
|
fluid
|
||||||
|
className={styles.clientLogo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
225
src/components/CoverflowCarousel.js
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import styles from './CoverflowCarousel.module.css';
|
||||||
|
import ProductCard from './ProductCard';
|
||||||
|
|
||||||
|
const CoverflowCarousel = ({ products, onCardClick }) => {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [animationState, setAnimationState] = useState('initial'); // 'initial', 'spread', 'ready'
|
||||||
|
const [shiftDirection, setShiftDirection] = useState(null); // 'left' | 'right' | null
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [edgeEnter, setEdgeEnter] = useState(null); // 'left' | 'right' | null
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const dragStartX = useRef(null);
|
||||||
|
const hasSwiped = useRef(false);
|
||||||
|
|
||||||
|
// Handle navigation
|
||||||
|
const goToProduct = (index, dir = null) => {
|
||||||
|
if (dir) {
|
||||||
|
setShiftDirection(dir);
|
||||||
|
// mark which edge is entering for a short time to trigger ease-in
|
||||||
|
setEdgeEnter(dir === 'left' ? 'right' : 'left');
|
||||||
|
setTimeout(() => setEdgeEnter(null), 80);
|
||||||
|
setTimeout(() => setShiftDirection(null), 820);
|
||||||
|
}
|
||||||
|
setCurrentIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextProduct = () => {
|
||||||
|
const nextIndex = (currentIndex + 1) % products.length;
|
||||||
|
goToProduct(nextIndex, 'right');
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevProduct = () => {
|
||||||
|
const prevIndex = (currentIndex - 1 + products.length) % products.length;
|
||||||
|
goToProduct(prevIndex, 'left');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag/Swipe handlers
|
||||||
|
const onPointerDown = (e) => {
|
||||||
|
const x = e.touches ? e.touches[0].clientX : e.clientX;
|
||||||
|
dragStartX.current = x;
|
||||||
|
hasSwiped.current = false;
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerMove = (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
const x = e.touches ? e.touches[0].clientX : e.clientX;
|
||||||
|
const dx = x - (dragStartX.current ?? x);
|
||||||
|
const threshold = 60;
|
||||||
|
if (!hasSwiped.current && Math.abs(dx) > threshold) {
|
||||||
|
if (dx > 0) {
|
||||||
|
prevProduct();
|
||||||
|
} else {
|
||||||
|
nextProduct();
|
||||||
|
}
|
||||||
|
hasSwiped.current = true;
|
||||||
|
dragStartX.current = x;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
dragStartX.current = null;
|
||||||
|
hasSwiped.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle dot navigation
|
||||||
|
const goToProductByIndex = (index) => {
|
||||||
|
if (index === currentIndex) return;
|
||||||
|
const dir = index > currentIndex ? 'right' : 'left';
|
||||||
|
goToProduct(index, dir);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collapse overlay for center card
|
||||||
|
const collapseOverlay = () => {
|
||||||
|
// Reset animation state to force collapse
|
||||||
|
setAnimationState('spread');
|
||||||
|
setTimeout(() => {
|
||||||
|
setAnimationState('ready');
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize carousel with spread effect when products are available
|
||||||
|
useEffect(() => {
|
||||||
|
if (!products || products.length === 0) return;
|
||||||
|
setAnimationState('initial');
|
||||||
|
const spreadTimer = setTimeout(() => {
|
||||||
|
setAnimationState('spread');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
const readyTimer = setTimeout(() => {
|
||||||
|
setAnimationState('ready');
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(spreadTimer);
|
||||||
|
clearTimeout(readyTimer);
|
||||||
|
};
|
||||||
|
}, [products.length]);
|
||||||
|
|
||||||
|
// Keyboard navigation
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e) => {
|
||||||
|
if (e.key === 'ArrowLeft') {
|
||||||
|
prevProduct();
|
||||||
|
} else if (e.key === 'ArrowRight') {
|
||||||
|
nextProduct();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, [currentIndex, products.length]);
|
||||||
|
|
||||||
|
// Don't render if no products
|
||||||
|
if (products.length === 0) {
|
||||||
|
return <div className={styles.container}>Tidak ada produk tersedia</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer 5-slot coverflow when data memadai; fallback ke jumlah ganjil saat produk < 5
|
||||||
|
let positions = [];
|
||||||
|
if (products.length >= 5) {
|
||||||
|
positions = [-2, -1, 0, 1, 2];
|
||||||
|
} else {
|
||||||
|
const visibleCountRaw = Math.min(5, products.length);
|
||||||
|
const visibleCount = visibleCountRaw % 2 === 0 ? Math.max(1, visibleCountRaw - 1) : visibleCountRaw;
|
||||||
|
const half = Math.floor(visibleCount / 2);
|
||||||
|
positions = Array.from({ length: visibleCount }, (_, i) => i - half);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`${styles.container} ${isDragging ? styles.dragging : ''}`}
|
||||||
|
onMouseDown={onPointerDown}
|
||||||
|
onMouseMove={onPointerMove}
|
||||||
|
onMouseUp={onPointerUp}
|
||||||
|
onMouseLeave={onPointerUp}
|
||||||
|
onTouchStart={onPointerDown}
|
||||||
|
onTouchMove={onPointerMove}
|
||||||
|
onTouchEnd={onPointerUp}
|
||||||
|
>
|
||||||
|
<div className={`${styles.carouselWrapper} ${shiftDirection === 'left' ? styles.shiftingLeft : ''} ${shiftDirection === 'right' ? styles.shiftingRight : ''}`}>
|
||||||
|
{positions.map((position) => {
|
||||||
|
const count = products.length;
|
||||||
|
const productIndex = (currentIndex + position + count) % count;
|
||||||
|
const product = products[productIndex];
|
||||||
|
|
||||||
|
// Determine position class (clamped to available classes)
|
||||||
|
let positionClass = '';
|
||||||
|
if (position <= -2) positionClass = styles.positionNeg2;
|
||||||
|
else if (position === -1) positionClass = styles.positionNeg1;
|
||||||
|
else if (position === 0) positionClass = styles.position0;
|
||||||
|
else if (position === 1) positionClass = styles.position1;
|
||||||
|
else if (position >= 2) positionClass = styles.position2;
|
||||||
|
|
||||||
|
// Determine entering class for edge items (works for 3 or 5 slots)
|
||||||
|
const maxEdge = positions.length > 0 ? Math.max(...positions) : 2; // 2 for 5 slots, 1 for 3 slots
|
||||||
|
const enteringClass = edgeEnter === 'right' && position === maxEdge
|
||||||
|
? styles.enterFromRight
|
||||||
|
: edgeEnter === 'left' && position === -maxEdge
|
||||||
|
? styles.enterFromLeft
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`prod_${(product && product.id != null) ? product.id : productIndex}`}
|
||||||
|
className={`${styles.cardContainer} ${positionClass} ${
|
||||||
|
animationState === 'initial' ? styles.initial :
|
||||||
|
animationState === 'spread' ? styles.spread : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
// Only trigger navigation if this is not the center card
|
||||||
|
// or if it's the center card but not in hover state (overlay not visible)
|
||||||
|
const isCenter = position === 0;
|
||||||
|
const canHover = isCenter && animationState === 'ready' && !shiftDirection && !isDragging;
|
||||||
|
if (position !== 0 || (position === 0 && (!canHover || animationState !== 'ready' || shiftDirection || isDragging))) {
|
||||||
|
goToProduct(productIndex, position > 0 ? 'right' : (position < 0 ? 'left' : null));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.cardShadow} aria-hidden="true"></div>
|
||||||
|
<div className={styles.cardWrapper}>
|
||||||
|
<ProductCard
|
||||||
|
product={product}
|
||||||
|
onCardClick={(p,d) => { onCardClick && onCardClick(p,d); }}
|
||||||
|
isCenter={position === 0}
|
||||||
|
canHover={position === 0 && animationState === 'ready' && !shiftDirection && !isDragging}
|
||||||
|
onCollapse={position === 0 ? collapseOverlay : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation buttons */}
|
||||||
|
<button className={styles.navButton} onClick={prevProduct}>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15 18L9 12L15 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button className={styles.navButton} onClick={nextProduct}>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9 18L15 12L9 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dots indicator */}
|
||||||
|
{products.length > 1 && (
|
||||||
|
<div className={styles.dotsContainer}>
|
||||||
|
{products.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className={`${styles.dot} ${index === currentIndex ? styles.active : ''}`}
|
||||||
|
onClick={() => goToProductByIndex(index)}
|
||||||
|
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CoverflowCarousel;
|
||||||
655
src/components/CoverflowCarousel.module.css
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 450px;
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: none;
|
||||||
|
margin: 40px 0 60px 0;
|
||||||
|
perspective: 1000px;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left and right fade out masks */
|
||||||
|
.leftMask,
|
||||||
|
.rightMask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100px;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftMask {
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(to right, #0b1220, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightMask {
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(to left, #0b1220, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselWrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
perspective: 1500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Entering edge helpers to ensure ease-out even with <5 products */
|
||||||
|
/* entering helpers removed (reverted) */
|
||||||
|
|
||||||
|
/* Edge fade-out on shift for better intuition */
|
||||||
|
.carouselWrapper.shiftingLeft .cardContainer.positionNeg2 {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-420px) rotateY(62deg) scale(0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselWrapper.shiftingRight .cardContainer.position2 {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(420px) rotateY(-62deg) scale(0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer {
|
||||||
|
position: absolute;
|
||||||
|
width: 320px;
|
||||||
|
height: 370px;
|
||||||
|
cursor: pointer;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
will-change: transform;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform-origin: center center;
|
||||||
|
transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
|
opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
transform: translateX(0) rotateY(0) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
aspect-ratio: 320/370;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardWrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 15px;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2; /* ensure content stacks above shadow */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ground shadow to enhance 3D standing effect */
|
||||||
|
.cardShadow {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 6px;
|
||||||
|
width: 92%;
|
||||||
|
height: 32px;
|
||||||
|
transform: translateX(-50%) scale(1);
|
||||||
|
background: radial-gradient(ellipse at center, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.32) 45%, rgba(0,0,0,0) 78%);
|
||||||
|
filter: blur(8px);
|
||||||
|
opacity: 0.45;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.position0 .cardShadow {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateX(-50%) scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.positionNeg1 .cardShadow,
|
||||||
|
.cardContainer.position1 .cardShadow {
|
||||||
|
opacity: 0.55;
|
||||||
|
transform: translateX(-50%) scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.positionNeg2 .cardShadow,
|
||||||
|
.cardContainer.position2 .cardShadow {
|
||||||
|
opacity: 0.42;
|
||||||
|
transform: translateX(-50%) scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Initial state: subtle pop-in */
|
||||||
|
.cardContainer.initial {
|
||||||
|
transform: translateX(0) translateY(20px) rotateY(0) scale(0.92);
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New load spread: compact fan-out from center */
|
||||||
|
.cardContainer.spread.positionNeg2 {
|
||||||
|
transform: translateX(-320px) translateZ(-120px) rotateY(10deg) scale(0.9);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.spread.positionNeg1 {
|
||||||
|
transform: translateX(-160px) translateZ(-60px) rotateY(6deg) scale(0.97);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.spread.position0 {
|
||||||
|
transform: translateX(0) translateZ(0) rotateY(0) scale(1.06);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.spread.position1 {
|
||||||
|
transform: translateX(160px) translateZ(-60px) rotateY(-6deg) scale(0.97);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.spread.position2 {
|
||||||
|
transform: translateX(320px) translateZ(-120px) rotateY(-10deg) scale(0.9);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position classes for coverflow effect (closer to center) */
|
||||||
|
.cardContainer.positionNeg2 {
|
||||||
|
transform: translateX(-380px) rotateY(55deg) scale(0.7);
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.positionNeg1 {
|
||||||
|
transform: translateX(-190px) rotateY(35deg) scale(0.8);
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.position0 {
|
||||||
|
transform: translateX(0) rotateY(0deg) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.position1 {
|
||||||
|
transform: translateX(190px) rotateY(-35deg) scale(0.8);
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.position2 {
|
||||||
|
transform: translateX(380px) rotateY(-55deg) scale(0.7);
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge-enter overrides (placed after position rules to win cascade) */
|
||||||
|
.cardContainer.enterFromRight.position1,
|
||||||
|
.cardContainer.enterFromRight.position2 {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(520px) rotateY(-62deg) scale(0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.enterFromLeft.positionNeg1,
|
||||||
|
.cardContainer.enterFromLeft.positionNeg2 {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-520px) rotateY(62deg) scale(0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardImage {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
position: relative;
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholderImage {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
object-fit: cover;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholderImage span {
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
padding: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardDescription {
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardFooter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
padding-top: 1rem;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceContainer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.freePrice {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paidPrice {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enrollButton {
|
||||||
|
background: linear-gradient(135deg, #6a59ff 0%, #8261ee 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(106, 89, 255, 0.2);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enrollButton:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(106, 89, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enrollButton:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.noCourses {
|
||||||
|
text-align: center;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingCourses {
|
||||||
|
text-align: center;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #64748b;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingSpinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 3px solid #e2e8f0;
|
||||||
|
border-top: 3px solid #6a59ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation buttons */
|
||||||
|
.navButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: white;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: #475569;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:hover:not(:disabled) {
|
||||||
|
border-color: #6a59ff;
|
||||||
|
color: #6a59ff;
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:first-of-type {
|
||||||
|
left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:last-of-type {
|
||||||
|
right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dots indicator */
|
||||||
|
.dotsContainer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -40px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #cbd5e1;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.active {
|
||||||
|
background-color: #6a59ff;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:hover {
|
||||||
|
background-color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
height: 430px;
|
||||||
|
margin: 35px 0 55px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer {
|
||||||
|
width: 300px;
|
||||||
|
height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:first-of-type {
|
||||||
|
left: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:last-of-type {
|
||||||
|
right: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotsContainer {
|
||||||
|
bottom: -35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftMask,
|
||||||
|
.rightMask {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.container {
|
||||||
|
height: 410px;
|
||||||
|
margin: 30px 0 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer {
|
||||||
|
width: 280px;
|
||||||
|
height: 330px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardWrapper {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:first-of-type {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:last-of-type {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotsContainer {
|
||||||
|
bottom: -30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftMask,
|
||||||
|
.rightMask {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
height: 380px;
|
||||||
|
margin: 25px 0 45px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer {
|
||||||
|
width: 250px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardWrapper {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:first-of-type {
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:last-of-type {
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotsContainer {
|
||||||
|
bottom: -25px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftMask,
|
||||||
|
.rightMask {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.container {
|
||||||
|
height: 320px;
|
||||||
|
margin: 20px 0 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer {
|
||||||
|
width: 200px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardWrapper {
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:first-of-type {
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton:last-of-type {
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotsContainer {
|
||||||
|
bottom: -20px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust positions for smaller screens */
|
||||||
|
.cardContainer.positionNeg2 {
|
||||||
|
transform: translateX(-280px) rotateY(55deg) scale(0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.positionNeg1 {
|
||||||
|
transform: translateX(-140px) rotateY(35deg) scale(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.position1 {
|
||||||
|
transform: translateX(140px) rotateY(-35deg) scale(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.position2 {
|
||||||
|
transform: translateX(280px) rotateY(-55deg) scale(0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftMask,
|
||||||
|
.rightMask {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.container {
|
||||||
|
height: 280px;
|
||||||
|
margin: 15px 0 35px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer {
|
||||||
|
width: 180px;
|
||||||
|
height: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotsContainer {
|
||||||
|
bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Further adjust positions for very small screens */
|
||||||
|
.cardContainer.positionNeg2 {
|
||||||
|
transform: translateX(-240px) rotateY(55deg) scale(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.positionNeg1 {
|
||||||
|
transform: translateX(-120px) rotateY(35deg) scale(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.position1 {
|
||||||
|
transform: translateX(120px) rotateY(-35deg) scale(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer.position2 {
|
||||||
|
transform: translateX(240px) rotateY(-55deg) scale(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftMask,
|
||||||
|
.rightMask {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
239
src/components/CreateProductPage.js
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styles from './Dashboard.module.css';
|
||||||
|
|
||||||
|
const CreateProductPage = ({ parentId = null, onSuccess, onCancel }) => {
|
||||||
|
const [availableTypes, setAvailableTypes] = useState([]);
|
||||||
|
const [availableGroups, setAvailableGroups] = useState([]);
|
||||||
|
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [image, setImage] = useState('');
|
||||||
|
const [price, setPrice] = useState('');
|
||||||
|
const [unitType, setUnitType] = useState('duration');
|
||||||
|
const [durationValue, setDurationValue] = useState('');
|
||||||
|
const [durationUnit, setDurationUnit] = useState('days');
|
||||||
|
const [quantity, setQuantity] = useState('');
|
||||||
|
const [selectedType, setSelectedType] = useState('');
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState('');
|
||||||
|
const [siteUrl, setSiteUrl] = useState('');
|
||||||
|
const [createUpdateUrl, setCreateUpdateUrl] = useState('');
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDistinctOptions = async () => {
|
||||||
|
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
||||||
|
if (!match) return;
|
||||||
|
const token = match[2];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
'https://bot.kediritechnopark.com/webhook/store-production/get-products',
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
const productsArr = result || [];
|
||||||
|
|
||||||
|
const types = [...new Set(productsArr.map((p) => p.type).filter(Boolean))];
|
||||||
|
const groups = [...new Set(productsArr.map((p) => p.group).filter(Boolean))];
|
||||||
|
|
||||||
|
setAvailableTypes(types);
|
||||||
|
setAvailableGroups(groups);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Gagal ambil produk:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDistinctOptions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sendDataToN8N = async () => {
|
||||||
|
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
||||||
|
if (!match) {
|
||||||
|
alert('Token tidak ditemukan. Silakan login kembali.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = match[2];
|
||||||
|
|
||||||
|
const isToken = unitType === 'token';
|
||||||
|
const payload = {
|
||||||
|
name,
|
||||||
|
type: selectedType,
|
||||||
|
image,
|
||||||
|
description,
|
||||||
|
price: price === '' ? null : parseInt(price, 10),
|
||||||
|
currency: 'IDR',
|
||||||
|
duration: isToken ? null : `${parseInt(durationValue || '0', 10)} ${durationUnit}`,
|
||||||
|
quantity: isToken ? parseInt(quantity || '0', 10) : null,
|
||||||
|
unit_type: unitType,
|
||||||
|
sub_product_of: parentId,
|
||||||
|
is_visible: isVisible,
|
||||||
|
group: selectedGroup || null,
|
||||||
|
site_url: siteUrl || null,
|
||||||
|
create_update_url: createUpdateUrl || null,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://bot.kediritechnopark.com/webhook/store-production/add-product',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert('Produk berhasil ditambahkan!');
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
} else {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('Response Error:', errorText);
|
||||||
|
alert('Gagal mengirim data: ' + response.status);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending data to n8n:', error);
|
||||||
|
alert('Terjadi kesalahan saat mengirim data.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.chartCard}>
|
||||||
|
<h3 className={styles.transactionsTitle}>
|
||||||
|
{parentId ? 'Tambah Sub-Produk' : 'Tambah Produk Baru'}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{step === 0 && (
|
||||||
|
<section className={styles.formSection}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>Nama Produk</label>
|
||||||
|
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>Deskripsi</label>
|
||||||
|
<textarea rows={3} value={description} onChange={(e) => setDescription(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>URL Gambar</label>
|
||||||
|
<input type="text" value={image} onChange={(e) => setImage(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<section className={styles.formSection}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>Harga</label>
|
||||||
|
<input type="number" value={price} onChange={(e) => setPrice(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>Jenis Unit</label>
|
||||||
|
<select value={unitType} onChange={(e) => setUnitType(e.target.value)}>
|
||||||
|
<option value="duration">Durasi</option>
|
||||||
|
<option value="token">Token</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{unitType === 'token' ? (
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>Jumlah Token</label>
|
||||||
|
<input type="number" value={quantity} onChange={(e) => setQuantity(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>Durasi</label>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', width: '100%' }}>
|
||||||
|
<input type="number" style={{ width: '100%' }} value={durationValue} onChange={(e) => setDurationValue(e.target.value)} />
|
||||||
|
<select value={durationUnit} onChange={(e) => setDurationUnit(e.target.value)}>
|
||||||
|
<option value="days">Hari</option>
|
||||||
|
<option value="weeks">Minggu</option>
|
||||||
|
<option value="months">Bulan</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.formGroup} style={{ display: 'flex', flexDirection: 'row', gap: '0.5rem' }}>
|
||||||
|
<input id="visible" type="checkbox" checked={isVisible} onChange={(e) => setIsVisible(e.target.checked)} />
|
||||||
|
<label htmlFor="visible" style={{ margin: 0 }}>Tampilkan produk</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<section className={styles.formSection}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>Tipe Produk</label>
|
||||||
|
<input type="text" value={selectedType} onChange={(e) => setSelectedType(e.target.value)} />
|
||||||
|
<div className={styles.suggestionContainer}>
|
||||||
|
{availableTypes.map((type) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
type="button"
|
||||||
|
className={styles.suggestionButton}
|
||||||
|
onClick={() => setSelectedType(type)}
|
||||||
|
>
|
||||||
|
{type}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>Group</label>
|
||||||
|
<input type="text" value={selectedGroup} onChange={(e) => setSelectedGroup(e.target.value)} />
|
||||||
|
<div className={styles.suggestionContainer}>
|
||||||
|
{availableGroups.map((group) => (
|
||||||
|
<button
|
||||||
|
key={group}
|
||||||
|
type="button"
|
||||||
|
className={styles.suggestionButton}
|
||||||
|
onClick={() => setSelectedGroup(group)}
|
||||||
|
>
|
||||||
|
{group}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>Site URL</label>
|
||||||
|
<input type="text" value={siteUrl} onChange={(e) => setSiteUrl(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label>CREATE UPDATE URL</label>
|
||||||
|
<input type="text" value={createUpdateUrl} onChange={(e) => setCreateUpdateUrl(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.formActions}>
|
||||||
|
|
||||||
|
|
||||||
|
<button type="button" className={styles.submitButton} style={{visibility: step < 1 ? 'hidden': 'visible' }} onClick={() => setStep((s) => s - 1)}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
{step < 2 ? (
|
||||||
|
<button type="button" className={styles.submitButton} onClick={() => setStep((s) => s + 1)}>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="button" className={styles.submitButton} onClick={sendDataToN8N}>
|
||||||
|
{parentId ? 'Buat Sub-Produk' : 'Buat Produk'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateProductPage;
|
||||||
191
src/components/Dashboard copy.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { TrendingUp, TrendingDown, DollarSign, ShoppingCart, Users, Plus, GitBranchPlus } from 'lucide-react';
|
||||||
|
import styles from './Dashboard.module.css';
|
||||||
|
import processProducts from '../helper/processProducts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props:
|
||||||
|
* - setShowedModal: (modalName: string, productId?: string|number) => void
|
||||||
|
*/
|
||||||
|
const Dashboard = ({ setShowedModal }) => {
|
||||||
|
const [unitType, setUnitType] = useState('duration'); // kept for potential reuse
|
||||||
|
const [durationUnit, setDurationUnit] = useState('days'); // kept for potential reuse
|
||||||
|
const [availableTypes, setAvailableTypes] = useState([]);
|
||||||
|
const [availableGroups, setAvailableGroups] = useState([]);
|
||||||
|
const [selectedType, setSelectedType] = useState(null);
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
const [products, setProducts] = useState([]);
|
||||||
|
|
||||||
|
const [dashboardData, setDashboardData] = useState({
|
||||||
|
totalRevenue: { amount: 10215845, currency: 'IDR', change: 33.87, period: '22 - 29 May 2025' },
|
||||||
|
totalItemsSold: { amount: 128980, change: -33.87, period: '22 - 29 May 2025' },
|
||||||
|
totalVisitors: { amount: 2905897, change: 33.87, period: '22 - 29 May 2025' },
|
||||||
|
chartData: [
|
||||||
|
{ date: '22/06', items: 200, revenue: 800 },
|
||||||
|
{ date: '23/06', items: 750, revenue: 450 },
|
||||||
|
{ date: '24/06', items: 550, revenue: 200 },
|
||||||
|
{ date: '25/06', items: 300, revenue: 350 },
|
||||||
|
{ date: '26/06', items: 900, revenue: 450 },
|
||||||
|
{ date: '27/06', items: 550, revenue: 200 },
|
||||||
|
],
|
||||||
|
latestTransactions: []
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDistinctOptions = async () => {
|
||||||
|
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
||||||
|
if (!match) return;
|
||||||
|
const token = match[2];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://bot.kediritechnopark.com/webhook/store-production/get-products', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
const productsArr = result || [];
|
||||||
|
|
||||||
|
const types = [...new Set(productsArr.map(p => p.type).filter(Boolean))];
|
||||||
|
const groups = [...new Set(productsArr.map(p => p.group).filter(Boolean))];
|
||||||
|
|
||||||
|
setAvailableTypes(types);
|
||||||
|
setAvailableGroups(groups);
|
||||||
|
setProducts(processProducts(productsArr));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Gagal ambil produk:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDistinctOptions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formatCurrency = (amount) => new Intl.NumberFormat('id-ID').format(amount);
|
||||||
|
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'confirmed': return styles.statusConfirmed;
|
||||||
|
case 'waiting payment': return styles.statusWaiting;
|
||||||
|
case 'payment expired': return styles.statusExpired;
|
||||||
|
default: return styles.statusConfirmed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatCard = ({ title, value, currency, change, period, icon: Icon, isNegative }) => (
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statCardHeader}>
|
||||||
|
<h3 className={styles.statCardTitle}>{title}</h3>
|
||||||
|
<Icon className={styles.statCardIcon} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCardValue}>
|
||||||
|
{currency && `${currency} `}{formatCurrency(value)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCardFooter}>
|
||||||
|
<div className={styles.statCardChange}>
|
||||||
|
{isNegative ? (
|
||||||
|
<TrendingDown className={`${styles.trendIcon} ${styles.trendDown}`} />
|
||||||
|
) : (
|
||||||
|
<TrendingUp className={`${styles.trendIcon} ${styles.trendUp}`} />
|
||||||
|
)}
|
||||||
|
<span className={`${styles.changeText} ${isNegative ? styles.changeTextNegative : styles.changeTextPositive}`}>
|
||||||
|
{Math.abs(change)}%
|
||||||
|
</span>
|
||||||
|
<span className={styles.fromLastWeek}>from last week</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCardPeriod}>{period}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const BarChart = ({ data }) => {
|
||||||
|
const maxValue = Math.max(...data.map(item => Math.max(item.items, item.revenue)));
|
||||||
|
return (
|
||||||
|
<div className={styles.barChart}>
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<div key={index} className={styles.barGroup}>
|
||||||
|
<div className={styles.barContainer}>
|
||||||
|
<div className={`${styles.bar} ${styles.barItems}`} style={{ height: `${(item.items / maxValue) * 200}px` }} />
|
||||||
|
<div className={`${styles.bar} ${styles.barRevenue}`} style={{ height: `${(item.revenue / maxValue) * 200}px` }} />
|
||||||
|
</div>
|
||||||
|
<span className={styles.barLabel}>{item.date}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.statsGrid}>
|
||||||
|
<StatCard title="Total Revenue" value={dashboardData.totalRevenue.amount} currency="IDR" change={dashboardData.totalRevenue.change} period={dashboardData.totalRevenue.period} icon={DollarSign} isNegative={false} />
|
||||||
|
<StatCard title="Total Items Sold" value={dashboardData.totalItemsSold.amount} change={dashboardData.totalItemsSold.change} period={dashboardData.totalItemsSold.period} icon={ShoppingCart} isNegative={true} />
|
||||||
|
<StatCard title="Total Visitor" value={dashboardData.totalVisitors.amount} change={dashboardData.totalVisitors.change} period={dashboardData.totalVisitors.period} icon={Users} isNegative={false} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.chartsGrid}>
|
||||||
|
{/* Tempatkan <BarChart data={dashboardData.chartData} /> jika mau ditampilkan */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Products List */}
|
||||||
|
<div className={styles.chartCard}>
|
||||||
|
<div className={styles.transactionsHeader}>
|
||||||
|
<h3 className={styles.transactionsTitle}>Products</h3>
|
||||||
|
|
||||||
|
{/* Tombol "Buat Item" → buka modal create */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.primaryButton}
|
||||||
|
onClick={() => setShowedModal && setShowedModal('create-item')}
|
||||||
|
title="Buat produk baru"
|
||||||
|
>
|
||||||
|
<Plus size={16} style={{ marginRight: 6 }} />
|
||||||
|
Buat Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.transactionsList}>
|
||||||
|
{products.map((product) => (
|
||||||
|
<div key={product.id} className={styles.transactionItem}>
|
||||||
|
<div className={styles.transactionLeft}>
|
||||||
|
<div className={styles.transactionInfo}>
|
||||||
|
<h4>{product.name}</h4>
|
||||||
|
{product.children && product.children.map((child) => (
|
||||||
|
<p key={child.id}>- {child.name}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.transactionRight}>
|
||||||
|
<span className={styles.transactionAmount}>
|
||||||
|
IDR {formatCurrency(product.price)}
|
||||||
|
</span>
|
||||||
|
<div className={`${styles.statusIndicator} ${getStatusClass(product.status)}`}></div>
|
||||||
|
<span className={styles.transactionStatus}>{product.status}</span>
|
||||||
|
|
||||||
|
{/* Tombol "Add Child" → buka modal create dengan parent product_id */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.secondaryButton}
|
||||||
|
onClick={() => setShowedModal('create-item', product.id)}
|
||||||
|
title="Tambah sub-produk"
|
||||||
|
style={{ marginLeft: '0.75rem' }}
|
||||||
|
>
|
||||||
|
<GitBranchPlus size={16} style={{ marginRight: 6 }} />
|
||||||
|
Add Child
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bagian form create yang lama sudah DIPINDAH ke halaman/komponen baru */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { TrendingUp, TrendingDown, DollarSign, ShoppingCart, Users } from 'lucide-react';
|
|
||||||
import styles from './Dashboard.module.css';
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
|
||||||
const [dashboardData, setDashboardData] = useState({
|
|
||||||
totalRevenue: {
|
|
||||||
amount: 10215845,
|
|
||||||
currency: 'IDR',
|
|
||||||
change: 33.87,
|
|
||||||
period: '22 - 29 May 2025'
|
|
||||||
},
|
|
||||||
totalItemsSold: {
|
|
||||||
amount: 128980,
|
|
||||||
change: -33.87,
|
|
||||||
period: '22 - 29 May 2025'
|
|
||||||
},
|
|
||||||
totalVisitors: {
|
|
||||||
amount: 2905897,
|
|
||||||
change: 33.87,
|
|
||||||
period: '22 - 29 May 2025'
|
|
||||||
},
|
|
||||||
chartData: [
|
|
||||||
{ date: '22/06', items: 200, revenue: 800 },
|
|
||||||
{ date: '23/06', items: 750, revenue: 450 },
|
|
||||||
{ date: '24/06', items: 550, revenue: 200 },
|
|
||||||
{ date: '24/06', items: 300, revenue: 350 },
|
|
||||||
{ date: '24/06', items: 900, revenue: 450 },
|
|
||||||
{ date: '24/06', items: 550, revenue: 200 },
|
|
||||||
{ date: '24/06', items: 700, revenue: 300 }
|
|
||||||
],
|
|
||||||
latestTransactions: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Samantha William',
|
|
||||||
amount: 250875,
|
|
||||||
date: 'May 22, 2025',
|
|
||||||
status: 'confirmed',
|
|
||||||
avatar: 'SW'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Kevin Anderson',
|
|
||||||
amount: 350620,
|
|
||||||
date: 'May 22, 2025',
|
|
||||||
status: 'waiting payment',
|
|
||||||
avatar: 'KA'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Angela Samantha',
|
|
||||||
amount: 870563,
|
|
||||||
date: 'May 22, 2025',
|
|
||||||
status: 'confirmed',
|
|
||||||
avatar: 'AS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Michael Smith',
|
|
||||||
amount: 653975,
|
|
||||||
date: 'May 22, 2025',
|
|
||||||
status: 'payment expired',
|
|
||||||
avatar: 'MS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: 'Jonathan Sebastian',
|
|
||||||
amount: 950000,
|
|
||||||
date: 'May 22, 2025',
|
|
||||||
status: 'confirmed',
|
|
||||||
avatar: 'JS'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function untuk connect ke n8n webhook
|
|
||||||
const connectToN8NWebhook = async (webhookUrl) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(webhookUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setDashboardData(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error connecting to n8n webhook:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function untuk send data ke n8n webhook
|
|
||||||
const sendDataToN8N = async (webhookUrl, data) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(webhookUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
console.log('Data sent successfully to n8n');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending data to n8n:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
|
||||||
return new Intl.NumberFormat('id-ID').format(amount);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusClass = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'confirmed':
|
|
||||||
return styles.statusConfirmed;
|
|
||||||
case 'waiting payment':
|
|
||||||
return styles.statusWaiting;
|
|
||||||
case 'payment expired':
|
|
||||||
return styles.statusExpired;
|
|
||||||
default:
|
|
||||||
return styles.statusConfirmed;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatCard = ({ title, value, currency, change, period, icon: Icon, isNegative }) => (
|
|
||||||
<div className={styles.statCard}>
|
|
||||||
<div className={styles.statCardHeader}>
|
|
||||||
<h3 className={styles.statCardTitle}>{title}</h3>
|
|
||||||
<Icon className={styles.statCardIcon} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.statCardValue}>
|
|
||||||
{currency && `${currency} `}{formatCurrency(value)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.statCardFooter}>
|
|
||||||
<div className={styles.statCardChange}>
|
|
||||||
{isNegative ? (
|
|
||||||
<TrendingDown className={`${styles.trendIcon} ${styles.trendDown}`} />
|
|
||||||
) : (
|
|
||||||
<TrendingUp className={`${styles.trendIcon} ${styles.trendUp}`} />
|
|
||||||
)}
|
|
||||||
<span className={`${styles.changeText} ${isNegative ? styles.changeTextNegative : styles.changeTextPositive}`}>
|
|
||||||
{Math.abs(change)}%
|
|
||||||
</span>
|
|
||||||
<span className={styles.fromLastWeek}>from last week</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.statCardPeriod}>{period}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const BarChart = ({ data }) => {
|
|
||||||
const maxValue = Math.max(...data.map(item => Math.max(item.items, item.revenue)));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.barChart}>
|
|
||||||
{data.map((item, index) => (
|
|
||||||
<div key={index} className={styles.barGroup}>
|
|
||||||
<div className={styles.barContainer}>
|
|
||||||
<div
|
|
||||||
className={`${styles.bar} ${styles.barItems}`}
|
|
||||||
style={{
|
|
||||||
height: `${(item.items / maxValue) * 200}px`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`${styles.bar} ${styles.barRevenue}`}
|
|
||||||
style={{
|
|
||||||
height: `${(item.revenue / maxValue) * 200}px`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className={styles.barLabel}>{item.date}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
{/* Stats Cards */}
|
|
||||||
<div className={styles.statsGrid}>
|
|
||||||
<StatCard
|
|
||||||
title="Total Revenue"
|
|
||||||
value={dashboardData.totalRevenue.amount}
|
|
||||||
currency={dashboardData.totalRevenue.currency}
|
|
||||||
change={dashboardData.totalRevenue.change}
|
|
||||||
period={dashboardData.totalRevenue.period}
|
|
||||||
icon={DollarSign}
|
|
||||||
isNegative={false}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title="Total Items Sold"
|
|
||||||
value={dashboardData.totalItemsSold.amount}
|
|
||||||
change={dashboardData.totalItemsSold.change}
|
|
||||||
period={dashboardData.totalItemsSold.period}
|
|
||||||
icon={ShoppingCart}
|
|
||||||
isNegative={true}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title="Total Visitor"
|
|
||||||
value={dashboardData.totalVisitors.amount}
|
|
||||||
change={dashboardData.totalVisitors.change}
|
|
||||||
period={dashboardData.totalVisitors.period}
|
|
||||||
icon={Users}
|
|
||||||
isNegative={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Charts and Transactions */}
|
|
||||||
<div className={styles.chartsGrid}>
|
|
||||||
{/* Report Statistics */}
|
|
||||||
<div className={styles.chartCard}>
|
|
||||||
<div className={styles.chartHeader}>
|
|
||||||
<div>
|
|
||||||
<h3 className={styles.chartTitle}>Report Statistics</h3>
|
|
||||||
<p className={styles.chartSubtitle}>Period: 22 - 29 May 2025</p>
|
|
||||||
</div>
|
|
||||||
<div className={styles.chartLegend}>
|
|
||||||
<div className={styles.legendItem}>
|
|
||||||
<div className={`${styles.legendColor} ${styles.legendColorGreen}`}></div>
|
|
||||||
<span className={styles.legendText}>Items Sold</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.legendItem}>
|
|
||||||
<div className={`${styles.legendColor} ${styles.legendColorLightGreen}`}></div>
|
|
||||||
<span className={styles.legendText}>Revenue</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<BarChart data={dashboardData.chartData} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Latest Transactions */}
|
|
||||||
<div className={styles.chartCard}>
|
|
||||||
<div className={styles.transactionsHeader}>
|
|
||||||
<h3 className={styles.transactionsTitle}>Latest Transactions</h3>
|
|
||||||
<a href="#" className={styles.seeAllLink}>see all transactions</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.transactionsList}>
|
|
||||||
{dashboardData.latestTransactions.map((transaction) => (
|
|
||||||
<div key={transaction.id} className={styles.transactionItem}>
|
|
||||||
<div className={styles.transactionLeft}>
|
|
||||||
<div className={styles.transactionAvatar}>
|
|
||||||
{transaction.avatar}
|
|
||||||
</div>
|
|
||||||
<div className={styles.transactionInfo}>
|
|
||||||
<h4>{transaction.name}</h4>
|
|
||||||
<p>on {transaction.date}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.transactionRight}>
|
|
||||||
<span className={styles.transactionAmount}>
|
|
||||||
IDR {formatCurrency(transaction.amount)}
|
|
||||||
</span>
|
|
||||||
<div className={`${styles.statusIndicator} ${getStatusClass(transaction.status)}`}></div>
|
|
||||||
<span className={styles.transactionStatus}>
|
|
||||||
{transaction.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Dashboard;
|
|
||||||
|
|||||||
@@ -117,6 +117,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chartCard {
|
.chartCard {
|
||||||
|
width: 100%;
|
||||||
|
color: black;
|
||||||
|
text-align: left;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
@@ -225,7 +228,6 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #111827;
|
color: #111827;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.seeAllLink {
|
.seeAllLink {
|
||||||
@@ -324,3 +326,70 @@
|
|||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup input,
|
||||||
|
.formGroup textarea {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton {
|
||||||
|
background-color: #2563eb;
|
||||||
|
color: white;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup select {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestionContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestionButton {
|
||||||
|
background-color: #eee;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestionButton:hover {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.formActions {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
160
src/components/FAQSection.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
|
import styles from './FAQSection.module.css';
|
||||||
|
|
||||||
|
const Accordion = ({ children, type = "single", collapsible = true, className = "" }) => {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.accordion} ${className}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccordionItem = ({ children, value, className = "" }) => {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.accordionItem} ${className}`} data-value={value}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccordionTrigger = ({ children, className = "", onClick, isExpanded }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`${styles.accordionTrigger} ${className} ${isExpanded ? styles.expanded : ''}`}
|
||||||
|
onClick={onClick}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
>
|
||||||
|
<span className={styles.triggerText}>{children}</span>
|
||||||
|
<svg
|
||||||
|
className={`${styles.chevron} ${isExpanded ? styles.rotated : ''}`}
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 6L8 10L12 6"
|
||||||
|
stroke={isExpanded ? "#0057b8" : "currentColor"}
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccordionContent = ({ children, className = "", isExpanded }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.accordionContent} ${className} ${isExpanded ? styles.expanded : ''}`}
|
||||||
|
style={{
|
||||||
|
maxHeight: isExpanded ? '500px' : '0',
|
||||||
|
opacity: isExpanded ? 1 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.contentInner}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FAQSection = () => {
|
||||||
|
const [activeItem, setActiveItem] = useState("item-1");
|
||||||
|
|
||||||
|
const handleToggle = (value) => {
|
||||||
|
if (activeItem === value) {
|
||||||
|
setActiveItem("");
|
||||||
|
} else {
|
||||||
|
setActiveItem(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const faqs = [
|
||||||
|
{
|
||||||
|
id: "item-1",
|
||||||
|
question: "Apa itu Kediri Technopark?",
|
||||||
|
answer: "Kediri Technopark adalah pusat pengembangan inovasi digital dan aplikasi untuk masyarakat dan pelaku usaha di Kediri. Kami menyediakan berbagai solusi teknologi untuk membantu bisnis berkembang di era digital."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item-2",
|
||||||
|
question: "Produk apa saja yang ditawarkan oleh Kediri Technopark?",
|
||||||
|
answer: "Kami menawarkan berbagai produk digital seperti platform Point of Sale (Kedai Master), aplikasi manajemen bisnis, solusi e-commerce, serta layanan pengembangan website dan aplikasi custom sesuai kebutuhan bisnis Anda."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item-3",
|
||||||
|
question: "Apakah ada program akademi untuk belajar teknologi?",
|
||||||
|
answer: "Ya, kami memiliki Academy Program yang dirancang untuk anak-anak dan remaja. Program ini mencakup berbagai bidang seperti pemrograman, robotika, desain grafis, pengembangan web, dan data science dengan pendekatan yang interaktif dan kreatif."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item-4",
|
||||||
|
question: "Bagaimana cara mendaftar program akademi?",
|
||||||
|
answer: "Anda dapat mendaftar melalui website kami dengan mengklik tombol 'Daftar' pada program yang diminati. Setelah itu, tim kami akan menghubungi Anda untuk proses selanjutnya. Beberapa program bahkan tersedia secara gratis."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item-5",
|
||||||
|
question: "Apakah ada biaya untuk menggunakan produk Kediri Technopark?",
|
||||||
|
answer: "Kami menawarkan berbagai paket dengan harga yang berbeda sesuai dengan kebutuhan bisnis Anda. Beberapa produk memiliki versi gratis dengan fitur dasar, dan paket berbayar dengan fitur yang lebih lengkap. Anda dapat melihat detail harga di halaman produk masing-masing."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item-6",
|
||||||
|
question: "Berapa lama waktu implementasi produk Kediri Technopark?",
|
||||||
|
answer: "Waktu implementasi tergantung pada kompleksitas kebutuhan bisnis Anda. Untuk produk standar seperti Kedai Master, implementasi bisa dilakukan dalam 1-3 hari kerja. Untuk solusi custom, waktu implementasi akan disesuaikan dengan kebutuhan spesifik Anda."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item-7",
|
||||||
|
question: "Apakah tersedia pelatihan penggunaan produk?",
|
||||||
|
answer: "Ya, kami menyediakan pelatihan gratis untuk penggunaan produk standar kami. Untuk paket berbayar, pelatihan akan disesuaikan dengan paket yang Anda pilih. Tim support kami juga selalu siap membantu jika Anda memiliki pertanyaan."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item-8",
|
||||||
|
question: "Bagaimana jika saya memiliki pertanyaan teknis?",
|
||||||
|
answer: "Anda dapat menghubungi tim support kami melalui email marketing@kediritechnopark.com atau melalui nomor WhatsApp 0813 1889 4994. Tim kami akan dengan senang hati membantu menjawab pertanyaan teknis Anda."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="faq" style={{scrollMarginTop: '65px'}} className={styles.faqSection}>
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<div className={styles.sectionHeading}>
|
||||||
|
<h2 className={styles.sectionTitle}>
|
||||||
|
FREQUENTLY ASKED <span className={styles.highlight}>QUESTIONS</span>
|
||||||
|
</h2>
|
||||||
|
<p className={styles.sectionDescription}>
|
||||||
|
Temukan jawaban untuk pertanyaan umum tentang layanan dan produk kami
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Accordion type="single" collapsible className={styles.accordionRoot}>
|
||||||
|
{faqs.map((faq) => (
|
||||||
|
<AccordionItem key={faq.id} value={faq.id} className={styles.accordionItemShadcn}>
|
||||||
|
<AccordionTrigger
|
||||||
|
isExpanded={activeItem === faq.id}
|
||||||
|
onClick={() => handleToggle(faq.id)}
|
||||||
|
className={styles.accordionTriggerShadcn}
|
||||||
|
>
|
||||||
|
{faq.question}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent
|
||||||
|
isExpanded={activeItem === faq.id}
|
||||||
|
className={styles.accordionContentShadcn}
|
||||||
|
>
|
||||||
|
<div className={styles.answer}>
|
||||||
|
{faq.answer}
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FAQSection;
|
||||||
214
src/components/FAQSection.module.css
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
.faqSection {
|
||||||
|
padding: 80px 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeading {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: #0057b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionDescription {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #64748b;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionRoot {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionItemShadcn {
|
||||||
|
border-radius: 0;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
background-color: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionItemShadcn:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionItemShadcn:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionTriggerShadcn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 18px 20px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #0f172a;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionTriggerShadcn:hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionTriggerShadcn:focus-visible {
|
||||||
|
outline: 2px solid #0057b8;
|
||||||
|
outline-offset: -2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerText {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
color: #94a3b8;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionContentShadcn {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.2s ease, opacity 0.2s ease;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionContentShadcn.expanded {
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentInner {
|
||||||
|
padding: 15px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer {
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.faqSection {
|
||||||
|
padding: 70px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.faqSection {
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionDescription {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionTriggerShadcn {
|
||||||
|
padding: 16px 18px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentInner {
|
||||||
|
padding: 0 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.faqSection {
|
||||||
|
padding: 50px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeading {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionTriggerShadcn {
|
||||||
|
padding: 15px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentInner {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.faqSection {
|
||||||
|
padding: 40px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionDescription {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionTriggerShadcn {
|
||||||
|
padding: 14px 15px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentInner {
|
||||||
|
padding: 0 15px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.faqSection {
|
||||||
|
padding: 35px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionTriggerShadcn {
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,74 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Container, Row, Col } from 'react-bootstrap';
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
|
import styles from './Footer.module.css';
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
return (
|
return (
|
||||||
<footer id="contact" className="bg-dark text-white py-4">
|
<footer className={styles.footer}>
|
||||||
<Container>
|
<Container>
|
||||||
<Row className="justify-content-center">
|
<Row className={styles.footerContent}>
|
||||||
<Col lg={6} className="text-center mb-3">
|
<Col lg={3} md={6} className={styles.footerColumn}>
|
||||||
<h4>Contact Us</h4>
|
<div className={styles.footerLogo}>
|
||||||
<p>Sunan Giri GG. I No. 11, Rejomulyo, Kediri, Jawa Timur 64129</p>
|
<img
|
||||||
<p><a href="tel:+6281318894994" className="text-white">0813 1889 4994</a></p>
|
src="https://kediritechnopark.com/kediri-technopark-logo-white.png"
|
||||||
<p><a href="mailto:marketing@kediritechnopark.com" className="text-white">marketing@kediritechnopark.com</a></p>
|
alt="Kediri Technopark Logo"
|
||||||
<p><a href="https://instagram.com/kediri.technopark" target="_blank" rel="noopener noreferrer" className="text-white">@kediri.technopark</a></p>
|
className={styles.logoImage}
|
||||||
<p><a href="https://kediritechnopark.com" target="_blank" rel="noopener noreferrer" className="text-white">www.KEDIRITECHNOPARK.com</a></p>
|
/>
|
||||||
<div className="mt-3">
|
</div>
|
||||||
<a href="https://wa.me/6281318894994" target="_blank" rel="noopener noreferrer" className="me-3 text-white fs-4">
|
<p className={styles.companyDescription}>
|
||||||
<i className="fab fa-whatsapp"></i>
|
Kediri Technopark adalah pusat pengembangan inovasi digital dan aplikasi untuk masyarakat dan pelaku usaha.
|
||||||
</a>
|
</p>
|
||||||
<a href="https://instagram.com/kediri.technopark" target="_blank" rel="noopener noreferrer" className="text-white fs-4">
|
<div className={styles.socialLinks}>
|
||||||
|
<a href="https://instagram.com/kediri.technopark" target="_blank" rel="noopener noreferrer" className={styles.socialLink}>
|
||||||
<i className="fab fa-instagram"></i>
|
<i className="fab fa-instagram"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://linkedin.com/company/kediri-technopark" target="_blank" rel="noopener noreferrer" className={styles.socialLink}>
|
||||||
|
<i className="fab fa-linkedin-in"></i>
|
||||||
|
</a>
|
||||||
|
<a href="https://facebook.com/kediritechnopark" target="_blank" rel="noopener noreferrer" className={styles.socialLink}>
|
||||||
|
<i className="fab fa-facebook-f"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg={6} className="text-center">
|
|
||||||
<div className="footer-widget">
|
<Col lg={3} md={6} className={styles.footerColumn}>
|
||||||
<h4>About Our Company</h4>
|
<h3 className={styles.footerTitle}>Contact Us</h3>
|
||||||
<div className="logo mb-3">
|
<div className={styles.contactInfo}>
|
||||||
<img src="/assets/images/logo-white.png" alt="Logo" className="img-fluid" />
|
<div className={styles.contactItem}>
|
||||||
|
<i className="fas fa-map-marker-alt"></i>
|
||||||
|
<span>Sunan Giri GG. I No. 11, Rejomulyo, Kediri, Jawa Timur 64129</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.contactItem}>
|
||||||
|
<i className="fas fa-phone"></i>
|
||||||
|
<span><a href="tel:+6281318894994">0813 1889 4994</a></span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.contactItem}>
|
||||||
|
<i className="fas fa-envelope"></i>
|
||||||
|
<span><a href="mailto:marketing@kediritechnopark.com">marketing@kediritechnopark.com</a></span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.contactItem}>
|
||||||
|
<i className="fas fa-globe"></i>
|
||||||
|
<span><a href="https://kediritechnopark.com" target="_blank" rel="noopener noreferrer">www.KEDIRITECHNOPARK.com</a></span>
|
||||||
</div>
|
</div>
|
||||||
<p>Kediri Technopark adalah pusat pengembangan inovasi digital dan aplikasi untuk masyarakat dan pelaku usaha.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg={12} className="text-center mt-3">
|
|
||||||
<div className="copyright-text">
|
<Col lg={3} md={6} className={styles.footerColumn}>
|
||||||
<p>
|
<h3 className={styles.footerTitle}>Newsletter</h3>
|
||||||
© 2025 Kediri Technopark. All Rights Reserved.<br />
|
<div className={styles.newsletter}>
|
||||||
Design by <a href="https://templatemo.com/" target="_blank" rel="noopener noreferrer" className="text-white">TemplateMo</a><br />
|
<p>Subscribe to our newsletter for the latest updates</p>
|
||||||
Distributed by <a href="https://themewagon.com/" target="_blank" rel="noopener noreferrer" className="text-white">ThemeWagon</a>
|
<div className={styles.newsletterForm}>
|
||||||
</p>
|
<input type="email" placeholder="Your email address" className={styles.newsletterInput} />
|
||||||
|
<button className={styles.newsletterButton}>Subscribe</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row className={styles.footerBottom}>
|
||||||
|
<Col lg={12}>
|
||||||
|
<div className={styles.copyright}>
|
||||||
|
<p>© 2025 Kediri Technopark. All Rights Reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
252
src/components/Footer.module.css
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
.footer {
|
||||||
|
background-color: #1e293b;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 50px 0 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerContent {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerColumn {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerLogo {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoImage {
|
||||||
|
max-width: 160px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companyDescription {
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialLinks {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialLink {
|
||||||
|
color: #ffffff;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialLink:hover {
|
||||||
|
color: #0057b8;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerTitle {
|
||||||
|
color: #f1f5f9;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactItem {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactItem i {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-top: 3px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactItem a {
|
||||||
|
color: #e2e8f0;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactItem a:hover {
|
||||||
|
color: #0057b8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerLinks {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 25px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerLinks li a {
|
||||||
|
color: #94a3b8;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
display: block;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerLinks li a:hover {
|
||||||
|
color: #0057b8;
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter p {
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletterForm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletterInput {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletterInput::placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletterInput:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0057b8;
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletterButton {
|
||||||
|
background: #0057b8;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletterButton:hover {
|
||||||
|
background: #004a9e;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerBottom {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright p {
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.footer {
|
||||||
|
padding: 40px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerContent {
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerColumn {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoImage {
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer {
|
||||||
|
padding: 35px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerContent {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerColumn {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerTitle {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletterForm {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletterInput {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.footer {
|
||||||
|
padding: 30px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerContent {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialLinks {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialLink {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletterForm {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletterInput,
|
||||||
|
.newsletterButton {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import styles from './Styles.module.css';
|
import styles from './Styles.module.css';
|
||||||
|
|
||||||
@@ -6,32 +6,111 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [hoveredNav, setHoveredNav] = useState(null);
|
const [hoveredNav, setHoveredNav] = useState(null);
|
||||||
const [menuOpen, setMenuOpen] = useState(false); // toggle mobile menu
|
const [menuOpen, setMenuOpen] = useState(false); // toggle mobile menu
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onScroll = () => setIsScrolled(window.scrollY > 10);
|
||||||
|
window.addEventListener('scroll', onScroll);
|
||||||
|
onScroll();
|
||||||
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollToId = (id) => {
|
||||||
|
// Ensure we are on home, then scroll to target id smoothly
|
||||||
|
navigate('/');
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
// Backward compatibility with refs passed from App for products/academy
|
||||||
|
if (id === 'products' && typeof scrollToProduct === 'function') scrollToProduct();
|
||||||
|
if (id === 'academy' && typeof scrollToCourse === 'function') scrollToCourse();
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close mobile menu when window is resized to desktop size
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (window.innerWidth > 600) {
|
||||||
|
setMenuOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.header}>
|
<header className={`${styles.header} ${isScrolled ? styles.headerScrolled : ''}`}>
|
||||||
<img src="./kediri-technopark-logo.png" className={styles.logo} alt="Logo" />
|
<img src="./kediri-technopark-logo.png" className={styles.logo} alt="Logo" />
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
|
{username &&
|
||||||
|
<>
|
||||||
<a
|
<a
|
||||||
className={`${styles.navLink} ${hoveredNav === 2 ? styles.navLinkHover : ''}`}
|
className={`${styles.navLink} ${hoveredNav === 2 ? styles.navLinkHover : ''}`}
|
||||||
onMouseEnter={() => setHoveredNav(2)}
|
onMouseEnter={() => setHoveredNav(2)}
|
||||||
onMouseLeave={() => setHoveredNav(null)}
|
onMouseLeave={() => setHoveredNav(null)}
|
||||||
onClick={() => navigate('/')}
|
onClick={() => { navigate('/'); setMenuOpen(false); }}
|
||||||
>
|
>
|
||||||
HOME
|
Home
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className={`${styles.navLink} ${hoveredNav === 3 ? styles.navLinkHover : ''}`}
|
className={`${styles.navLink} ${hoveredNav === 3 ? styles.navLinkHover : ''}`}
|
||||||
onMouseEnter={() => setHoveredNav(3)}
|
onMouseEnter={() => setHoveredNav(3)}
|
||||||
onMouseLeave={() => setHoveredNav(null)}
|
onMouseLeave={() => setHoveredNav(null)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!username) scrollToCourse();
|
navigate('/dashboard');
|
||||||
else navigate('/products');
|
setMenuOpen(false);
|
||||||
}}
|
}}>
|
||||||
>
|
Dashboard
|
||||||
{username ? 'MY PRODUCTS' : 'PRODUCTS'}
|
|
||||||
</a>
|
</a>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{!username &&
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
className={`${styles.navLink} ${hoveredNav === 21 ? styles.navLinkHover : ''}`}
|
||||||
|
onMouseEnter={() => setHoveredNav(21)}
|
||||||
|
onMouseLeave={() => setHoveredNav(null)}
|
||||||
|
onClick={() => { scrollToId('about'); setMenuOpen(false); }}
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className={`${styles.navLink} ${hoveredNav === 22 ? styles.navLinkHover : ''}`}
|
||||||
|
onMouseEnter={() => setHoveredNav(22)}
|
||||||
|
onMouseLeave={() => setHoveredNav(null)}
|
||||||
|
onClick={() => { scrollToId('services'); setMenuOpen(false); }}
|
||||||
|
>
|
||||||
|
Services
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className={`${styles.navLink} ${hoveredNav === 3 ? styles.navLinkHover : ''}`}
|
||||||
|
onMouseEnter={() => setHoveredNav(3)}
|
||||||
|
onMouseLeave={() => setHoveredNav(null)}
|
||||||
|
onClick={() => { scrollToId('products'); setMenuOpen(false); }}
|
||||||
|
>
|
||||||
|
Products
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className={`${styles.navLink} ${hoveredNav === 4 ? styles.navLinkHover : ''}`}
|
||||||
|
onMouseEnter={() => setHoveredNav(4)}
|
||||||
|
onMouseLeave={() => setHoveredNav(null)}
|
||||||
|
onClick={() => { scrollToId('academy'); setMenuOpen(false); }}
|
||||||
|
>
|
||||||
|
Academy
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className={`${styles.navLink} ${hoveredNav === 5 ? styles.navLinkHover : ''}`}
|
||||||
|
onMouseEnter={() => setHoveredNav(5)}
|
||||||
|
onMouseLeave={() => setHoveredNav(null)}
|
||||||
|
onClick={() => { scrollToId('faq'); setMenuOpen(false); }}
|
||||||
|
>
|
||||||
|
FAQ
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Burger Menu Button */}
|
{/* Burger Menu Button */}
|
||||||
@@ -45,11 +124,17 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
|||||||
{username ? (
|
{username ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles.username}>{username}</div>
|
<div className={styles.username}>{username}</div>
|
||||||
|
<button onClick={() => { setMenuOpen(false); navigate('/'); }}>Home</button>
|
||||||
|
<button onClick={() => { setMenuOpen(false); scrollToId('about'); }}>About</button>
|
||||||
|
<button onClick={() => { setMenuOpen(false); scrollToId('services'); }}>Services</button>
|
||||||
|
<button onClick={() => { setMenuOpen(false); scrollToId('products'); }}>Products</button>
|
||||||
|
<button onClick={() => { setMenuOpen(false); scrollToId('academy'); }}>Academy</button>
|
||||||
|
<button onClick={() => { setMenuOpen(false); scrollToId('faq'); }}>FAQ</button>
|
||||||
<button className={styles.logoutButton} onClick={() => {
|
<button className={styles.logoutButton} onClick={() => {
|
||||||
navigate('/products');
|
navigate('/dashboard');
|
||||||
|
setMenuOpen(false);
|
||||||
}}>
|
}}>
|
||||||
MY PRODUCTS
|
Dashboard
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button className={styles.logoutButton} onClick={() => {
|
<button className={styles.logoutButton} onClick={() => {
|
||||||
@@ -60,6 +145,13 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<button onClick={() => { setMenuOpen(false); navigate('/'); }}>Home</button>
|
||||||
|
<button onClick={() => { setMenuOpen(false); scrollToId('about'); }}>About</button>
|
||||||
|
<button onClick={() => { setMenuOpen(false); scrollToId('services'); }}>Services</button>
|
||||||
|
<button onClick={() => { setMenuOpen(false); scrollToId('products'); }}>Products</button>
|
||||||
|
<button onClick={() => { setMenuOpen(false); scrollToId('academy'); }}>Academy</button>
|
||||||
|
<button onClick={() => { setMenuOpen(false); scrollToId('faq'); }}>FAQ</button>
|
||||||
<button
|
<button
|
||||||
className={styles.loginButton}
|
className={styles.loginButton}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -67,8 +159,9 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
|||||||
setShowedModal('login');
|
setShowedModal('login');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
LOGIN
|
Sign in
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -83,7 +176,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
|||||||
)}
|
)}
|
||||||
{!username && (
|
{!username && (
|
||||||
<button className={styles.loginButton} onClick={() => setShowedModal('login')}>
|
<button className={styles.loginButton} onClick={() => setShowedModal('login')}>
|
||||||
LOGIN
|
Sign in
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,79 @@
|
|||||||
import React from 'react';
|
import { Container, Row, Col, Button } from "react-bootstrap";
|
||||||
import { Container, Row, Col, Button } from 'react-bootstrap';
|
import styles from "./HeroSection.module.css";
|
||||||
|
|
||||||
const HeroSection = () => {
|
const HeroSection = ({ scrollToProduct, scrollToCourse }) => {
|
||||||
return (
|
return (
|
||||||
<section className="hero-section pt-5 bg-light">
|
<section
|
||||||
<Container>
|
className={`${styles.hero} pt-3`}
|
||||||
<Row className="align-items-center">
|
aria-label="Kediri Technopark hero section"
|
||||||
<Col lg={6}>
|
>
|
||||||
<h1>KATALIS KARIR DAN BISNIS DIGITAL</h1>
|
<Container className={styles.heroContainer}>
|
||||||
<p>Kami adalah ekosistem tempat mimpi digital tumbuh dan masa depan dibentuk. Di sinilah semangat belajar bertemu dengan inovasi, dan ide-ide muda diberi ruang untuk berkembang. Lebih dari sekadar tempat, kami adalah rumah bagi talenta, teknologi, dan transformasi. Mari jelajahi dunia digital, bangun karir, dan ciptakan solusi — semua dimulai dari sini.</p>
|
<Row className="align-items-center gy-3">
|
||||||
<div className="d-flex gap-3">
|
{/* Text first for mobile and desktop for clarity */}
|
||||||
<Button variant="outline-primary" href="https://instagram.com/kediri.technopark" target="_blank">Instagram</Button>
|
<Col
|
||||||
<Button variant="outline-success" href="tel:+6281318894994">WhatsApp</Button>
|
xs={{ order: 0 }}
|
||||||
|
lg={{ span: 8, order: 1 }}
|
||||||
|
xl={{ span: 7, order: 1 }}
|
||||||
|
>
|
||||||
|
<div className={styles.copyWrap}>
|
||||||
|
<h1 className={styles.title}>KATALIS KARIR DAN BISNIS DIGITAL</h1>
|
||||||
|
<p className={styles.lead}>
|
||||||
|
Kami adalah ekosistem tempat mimpi digital tumbuh dan masa depan
|
||||||
|
dibentuk. Di sinilah semangat belajar bertemu dengan inovasi,
|
||||||
|
dan ide-ide muda diberi ruang untuk berkembang. Lebih dari
|
||||||
|
sekadar tempat, kami adalah rumah bagi talenta, teknologi, dan
|
||||||
|
transformasi. Mari jelajahi dunia digital, bangun karir, dan
|
||||||
|
ciptakan solusi — semua dimulai dari sini.
|
||||||
|
</p>
|
||||||
|
<div className={styles.ctaGroup}>
|
||||||
|
<Button className={styles.ctaPrimary} onClick={scrollToProduct}>
|
||||||
|
Explore Products
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
className={styles.ctaSecondary}
|
||||||
|
onClick={scrollToCourse}
|
||||||
|
>
|
||||||
|
View Academy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg={6}>
|
<Col
|
||||||
<img src="https://kediritechnopark.com/assets/images/gambar1.png" alt="Hero Image" className="img-fluid" />
|
xs={{ order: 1 }}
|
||||||
|
lg={{ span: 4, order: 2 }}
|
||||||
|
xl={{ span: 5, order: 2 }}
|
||||||
|
>
|
||||||
|
<div className={styles.imageWrap}>
|
||||||
|
<div className={styles.imageFrame}>
|
||||||
|
<video
|
||||||
|
className={`img-fluid ${styles.heroImage}`}
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
>
|
||||||
|
<source src="/maya-idle.mp4" type="video/mp4" />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
</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>
|
||||||
|
|||||||
467
src/components/HeroSection.module.css
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
background: radial-gradient(
|
||||||
|
900px 400px at 0% -10%,
|
||||||
|
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;
|
||||||
|
min-height: clamp(300px, 40svh, 450px);
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroContainer {
|
||||||
|
max-width: 1280px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyWrap {
|
||||||
|
max-width: 600px;
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kickerRow {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.kickerBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: color-mix(in srgb, var(--brand) 12%, #fff);
|
||||||
|
color: var(--brand);
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.15;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
/* Fluid type: min 24px → max 40px, ensure 1-line on desktop */
|
||||||
|
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%
|
||||||
|
);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead {
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: clamp(0.95rem, 0.5vw + 0.9rem, 1.1rem);
|
||||||
|
max-width: 60ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletList {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: #334155; /* slate-700 */
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletIcon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--brand);
|
||||||
|
box-shadow: inset 0 0 0 2px #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaGroup {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaPrimary {
|
||||||
|
background: var(--brand) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border: 1px solid var(--brand) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 0.45rem 0.8rem !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
transition: background-color 0.16s ease, border-color 0.16s ease;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
box-shadow: var(--shadow-neutral-s);
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaPrimary:hover {
|
||||||
|
background: var(--brand-600) !important;
|
||||||
|
border-color: var(--brand-600) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaSecondary {
|
||||||
|
background: transparent !important;
|
||||||
|
color: var(--brand) !important;
|
||||||
|
border: 1px solid var(--brand) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 0.45rem 0.8rem !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
transition: color 0.16s ease, border-color 0.16s ease,
|
||||||
|
background-color 0.16s ease;
|
||||||
|
box-shadow: var(--shadow-neutral-s);
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaSecondary:hover {
|
||||||
|
color: #fff !important;
|
||||||
|
background: #3399ff !important; /* Lebih muda dari brand-600 */
|
||||||
|
border-color: #3399ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaPrimary:hover,
|
||||||
|
.ctaPrimary:focus-visible,
|
||||||
|
.ctaSecondary:hover,
|
||||||
|
.ctaSecondary:focus-visible {
|
||||||
|
box-shadow: var(--shadow-neutral-s);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageWrap {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
isolation: isolate;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 650px;
|
||||||
|
margin-left: auto;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageWrap::before,
|
||||||
|
.imageWrap::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageFrame {
|
||||||
|
position: relative;
|
||||||
|
border-radius: calc(var(--radius-2xl) + 6px);
|
||||||
|
overflow: hidden;
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
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;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroImage {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: cover;
|
||||||
|
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 {
|
||||||
|
position: absolute;
|
||||||
|
inset: auto 10% -10% 10%;
|
||||||
|
height: 40%;
|
||||||
|
filter: blur(40px);
|
||||||
|
z-index: -1;
|
||||||
|
background: radial-gradient(
|
||||||
|
60% 60% at 50% 0%,
|
||||||
|
color-mix(in srgb, var(--brand) 30%, transparent),
|
||||||
|
transparent 60%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statItem strong {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.statDot {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.copyWrap {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
.hero {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
.ctaGroup {
|
||||||
|
display: grid;
|
||||||
|
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) {
|
||||||
|
.imageWrap::before,
|
||||||
|
.imageWrap::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: clamp(1.4rem, 2vw + 1rem, 2.1rem);
|
||||||
|
line-height: 1.12;
|
||||||
|
}
|
||||||
|
.lead {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.imageWrap {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.hero {
|
||||||
|
padding-top: 0.8rem;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
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;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.copyWrap {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,10 @@ const KnowledgeBaseSection = () => {
|
|||||||
<section id="knowledge" className="knowledge section pt-5">
|
<section id="knowledge" className="knowledge section pt-5">
|
||||||
<Container>
|
<Container>
|
||||||
<Row>
|
<Row>
|
||||||
<Col lg={{ span: 8, offset: 2 }}>
|
<Col >
|
||||||
<div className="section-heading text-center mb-4">
|
<div className="section-heading mb-4">
|
||||||
<h4>KNOWLEDGE <em>BASE</em></h4>
|
<h4>KNOWLEDGE <em>BASE</em></h4>
|
||||||
<img src="/assets/images/heading-line-dec.png" alt="" className="mb-3" />
|
{/* <img src="/assets/images/heading-line-dec.png" alt="" className="mb-3" /> */}
|
||||||
<p>Berbagai artikel dan panduan untuk membantu Anda memahami teknologi dan inovasi digital.</p>
|
<p>Berbagai artikel dan panduan untuk membantu Anda memahami teknologi dan inovasi digital.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="knowledge-content">
|
<div className="knowledge-content">
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
|
const LoginRegister = ({setShowedModal}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [tab, setTab] = useState('login'); // 'login' or 'register'
|
const [tab, setTab] = useState('login'); // 'login' or 'register'
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
@@ -11,8 +13,7 @@ const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
|
|||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
padding: '2rem',
|
padding: '2rem',
|
||||||
maxWidth: '400px',
|
width: '100%',
|
||||||
margin: '0 auto',
|
|
||||||
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)',
|
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)',
|
||||||
fontFamily: 'Inter, system-ui, sans-serif',
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
},
|
},
|
||||||
@@ -78,7 +79,7 @@ const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('https://bot.kediritechnopark.com/webhook/user-dev/login', {
|
const res = await fetch('https://bot.kediritechnopark.com/webhook/user-production/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
@@ -95,11 +96,22 @@ const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
|
|||||||
if (token) {
|
if (token) {
|
||||||
document.cookie = `token=${token}; path=/; max-age=${7 * 24 * 60 * 60}`;
|
document.cookie = `token=${token}; path=/; max-age=${7 * 24 * 60 * 60}`;
|
||||||
|
|
||||||
if (postLoginAction) {
|
const params = new URLSearchParams(window.location.search);
|
||||||
postLoginAction(); // resume action (e.g., checkout)
|
const nextAction = params.get('next');
|
||||||
setPostLoginAction(null);
|
|
||||||
|
if (nextAction === 'checkout') {
|
||||||
|
params.delete('next');
|
||||||
|
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
||||||
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
setShowedModal('product');
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const modalType = params.get('modal');
|
||||||
|
if(!modalType)
|
||||||
|
navigate('/dashboard');
|
||||||
|
window.location.reload();
|
||||||
}
|
}
|
||||||
// window.location.reload()
|
|
||||||
} else {
|
} else {
|
||||||
alert('Token tidak ditemukan pada respons login');
|
alert('Token tidak ditemukan pada respons login');
|
||||||
}
|
}
|
||||||
@@ -117,7 +129,7 @@ const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('https://bot.kediritechnopark.com/webhook/user-dev/register', {
|
const res = await fetch('https://bot.kediritechnopark.com/webhook/user-production/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email, username, password }),
|
body: JSON.stringify({ email, username, password }),
|
||||||
|
|||||||
54
src/components/ProductCard.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './ProductCard.module.css';
|
||||||
|
|
||||||
|
const ProductCard = ({ product, onCardClick, isCenter, canHover, onCollapse }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.card} ${isCenter ? styles.isCenter : ''} ${canHover ? styles.canHover : ''}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className={styles.cover}
|
||||||
|
onError={(e) => { e.currentTarget.src = '/assets/images/placeholder-product.png'; }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.overlay}
|
||||||
|
onClick={(e) => {
|
||||||
|
// Collapse overlay when clicking on the overlay background (not buttons)
|
||||||
|
if (isCenter && canHover && onCollapse) {
|
||||||
|
// Check if the click target is the overlay itself, not a button
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onCollapse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.overlayInner}>
|
||||||
|
<h3 className={styles.title}>{product.name}</h3>
|
||||||
|
<div className={styles.meta}>
|
||||||
|
<p className={styles.description}>{product.description}</p>
|
||||||
|
<div className={styles.buttonGroup}>
|
||||||
|
<button
|
||||||
|
className={styles.detailButton}
|
||||||
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onCardClick && onCardClick(product, true); }}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</button>
|
||||||
|
{/* <button
|
||||||
|
className={styles.buyButton}
|
||||||
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onCardClick && onCardClick(product); }}
|
||||||
|
>
|
||||||
|
Beli
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductCard;
|
||||||
173
src/components/ProductCard.module.css
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
/* ProductCard.module.css - Cover with bottom-center overlay */
|
||||||
|
.card {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #0b1220;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 12px 28px rgba(2, 6, 23, 0.18), 0 6px 12px rgba(2, 6, 23, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.45s ease, filter 0.45s ease;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canHover:hover .cover {
|
||||||
|
transform: scale(1.04);
|
||||||
|
filter: brightness(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
--overlay-collapsed: 80px; /* raised to fit 2-line title */
|
||||||
|
--overlay-expanded: 58%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; bottom: 0;
|
||||||
|
height: var(--overlay-collapsed);
|
||||||
|
/* Lighter, wider gradient for a smoother look */
|
||||||
|
background: linear-gradient(180deg, rgba(2,6,23,0.00) 0%, rgba(0, 0, 0, 0.4) 50%, rgba(0, 0, 0, 0.65) 100%);
|
||||||
|
color: #fff;
|
||||||
|
transition: height 0.45s ease, background 0.45s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canHover:hover .overlay {
|
||||||
|
height: var(--overlay-expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayInner {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-shadow: 0 2px 8px rgba(0,0,0,0.35);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: max-height 0.45s ease, opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canHover:hover .meta {
|
||||||
|
max-height: 180px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 8px 0 10px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #e2e8f0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonGroup {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailButton,
|
||||||
|
.buyButton {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailButton {
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid rgba(255,255,255,0.28);
|
||||||
|
}
|
||||||
|
.detailButton:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
background: rgba(255,255,255,0.18);
|
||||||
|
border-color: rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buyButton {
|
||||||
|
background: var(--brand);
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buyButton:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
background: var(--brand-600);
|
||||||
|
border-color: var(--brand-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.title { font-size: 0.95rem; }
|
||||||
|
.description { -webkit-line-clamp: 2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.title { font-size: 0.9rem; }
|
||||||
|
.overlay { --overlay-collapsed: 60px; }
|
||||||
|
.description { font-size: 0.8rem; }
|
||||||
|
.buttonGroup { gap: 6px; }
|
||||||
|
.detailButton,
|
||||||
|
.buyButton {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.title { font-size: 0.85rem; }
|
||||||
|
.overlay { --overlay-collapsed: 50px; }
|
||||||
|
.overlayInner { padding: 10px 12px 12px; }
|
||||||
|
.description { font-size: 0.75rem; margin: 6px 0 8px; }
|
||||||
|
.buttonGroup { gap: 5px; }
|
||||||
|
.detailButton,
|
||||||
|
.buyButton {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.title { font-size: 0.8rem; }
|
||||||
|
.overlay { --overlay-collapsed: 45px; }
|
||||||
|
.overlayInner { padding: 8px 10px 10px; }
|
||||||
|
.description { font-size: 0.7rem; margin: 4px 0 6px; }
|
||||||
|
.buttonGroup { gap: 4px; }
|
||||||
|
.detailButton,
|
||||||
|
.buyButton {
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
width: 40vw;
|
width: 100%;
|
||||||
height: 260px;
|
height: 260px;
|
||||||
background-color: #e2e8f0;
|
background-color: #e2e8f0;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
@@ -22,17 +22,17 @@
|
|||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: contain;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerRow {
|
.headerRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 0.5rem; /* optional: supaya nggak terlalu rapat saat wrap */
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonGroup {
|
.buttonGroup {
|
||||||
@@ -90,16 +91,9 @@
|
|||||||
.container {
|
.container {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.headerRow {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
.buttonGroup {
|
.buttonGroup {
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
|
||||||
width: 63vw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.childSelector {
|
.childSelector {
|
||||||
|
|||||||
@@ -1,76 +1,115 @@
|
|||||||
import React, { useState } 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 = ({ subscriptions, product, setPostLoginAction, 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 [customName, setCustomName] = useState('');
|
|
||||||
|
|
||||||
const parseJWT = (token) => {
|
const navigate = useNavigate();
|
||||||
try {
|
|
||||||
const base64Url = token.split('.')[1];
|
const [customName, setCustomName] = useState('');
|
||||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
const [status, setStatus] = useState('idle'); // 'idle' | 'checking' | 'available' | 'unavailable' | 'error'
|
||||||
const jsonPayload = decodeURIComponent(
|
|
||||||
atob(base64)
|
|
||||||
.split('')
|
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
|
||||||
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
|
||||||
.join('')
|
|
||||||
);
|
|
||||||
return JSON.parse(jsonPayload);
|
// Helper panggil API kamu (GET + token header)
|
||||||
} catch {
|
async function checkProductAvailability(name, token) {
|
||||||
return null;
|
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] : '';
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setPostLoginAction(() => () => setShowedModal('product'));
|
requestLogin('checkout');
|
||||||
setShowedModal('login');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (product.type == 'product') {
|
|
||||||
|
if (product.type === 'product') {
|
||||||
const hasMatchingSubscription = Array.isArray(subscriptions) &&
|
const hasMatchingSubscription = Array.isArray(subscriptions) &&
|
||||||
subscriptions.some(sub =>
|
subscriptions.some(sub =>
|
||||||
sub.product_id === product.id || sub.product_parent_id === product.id
|
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
|
||||||
);
|
);
|
||||||
|
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) {
|
if (hasMatchingSubscription) {
|
||||||
const matching = subscriptions.filter(sub =>
|
const matching = subscriptions.filter(sub =>
|
||||||
sub.product_id === product.id || sub.product_parent_id === product.id
|
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (matching.length > 0) {
|
if (matching.length > 0 && !product.end_date) {
|
||||||
// ✅ 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) {
|
|
||||||
const matching = subscriptions.filter(sub =>
|
|
||||||
sub.product_id === product.id || sub.product_parent_id === product.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (matching.length > 0) {
|
|
||||||
const uniqueByName = Array.from(
|
const uniqueByName = Array.from(
|
||||||
new Map(matching.map(sub => [sub.product_name, sub])).values()
|
new Map(matching.map(sub => [sub.product_name, sub])).values()
|
||||||
);
|
);
|
||||||
@@ -78,47 +117,62 @@ if (hasMatchingSubscription) {
|
|||||||
setMatchingSubscriptions(uniqueByName);
|
setMatchingSubscriptions(uniqueByName);
|
||||||
setShowSubscriptionSelector(true);
|
setShowSubscriptionSelector(true);
|
||||||
return;
|
return;
|
||||||
}
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
// No children, no matching subscription
|
|
||||||
const itemsParam = JSON.stringify([product.id]);
|
const itemsParam = JSON.stringify([product.id]);
|
||||||
window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${product.name}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ If no subscription → ask for new product name
|
||||||
|
setShowNamingInput(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: direct checkout
|
||||||
|
const itemsParam = JSON.stringify([product.id]);
|
||||||
|
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;
|
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 = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ 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) {
|
||||||
|
// don’t 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 = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConfirmSelector = () => {
|
const onConfirmSelector = () => {
|
||||||
@@ -127,7 +181,7 @@ if (hasMatchingSubscription) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedSubscriptionId === product.id) {
|
if (selectedSubscriptionId === 0) {
|
||||||
setShowNamingInput(true);
|
setShowNamingInput(true);
|
||||||
} else {
|
} else {
|
||||||
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
|
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
|
||||||
@@ -140,59 +194,114 @@ if (hasMatchingSubscription) {
|
|||||||
const productName = selectedSubscription?.product_name;
|
const productName = selectedSubscription?.product_name;
|
||||||
const encodedName = encodeURIComponent(productName);
|
const encodedName = encodeURIComponent(productName);
|
||||||
|
|
||||||
window.location.href = `http://localhost:3002/?token=${token}&itemsId=${itemsParam}&set_name=${encodedName}&redirect_uri=http://localhost:3000/products&redirect_failed=http://localhost:3000`;
|
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${encodedName}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!product.executeCheckout && willDo === 'checkout') {
|
||||||
|
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('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
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 */}
|
||||||
{!showChildSelector && !showSubscriptionSelector && !showNamingInput && (
|
{!showChildSelector && !showSubscriptionSelector && !showNamingInput && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.image} style={{ backgroundImage: `url(${product.image})` }}></div>
|
<div className={styles.image} style={{ backgroundImage: `url(${product.image})` }}></div>
|
||||||
<div className={styles.headerRow}>
|
<div className={styles.headerRow}>
|
||||||
<h2 className={styles.title}>{product.name}</h2>
|
<h2 className={styles.title}>{product.name.split('%%%')[0]}</h2>
|
||||||
<div className={styles.price} style={{ color: priceColor }}>
|
<div className={styles.price} style={{ color: priceColor }}>
|
||||||
{product.price == null ? 'Pay-As-You-Go' : `Rp ${parseInt(product.price).toLocaleString('id-ID')}`}
|
{product.price == null ? 'Pay-As-You-Go' : `Rp ${parseInt(product.price).toLocaleString('id-ID')}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.description}>{product.description}</p>
|
<p className={styles.description}>{product.description}</p>
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
|
{(product.site_url || product.end_date || product.quantity) && (
|
||||||
|
<button
|
||||||
|
className={`${styles.button} ${styles.checkoutButton}`}
|
||||||
|
onClick={() => {
|
||||||
|
const token = (document.cookie.split('; ').find(row => row.startsWith('token=')) || '').split('=')[1] || '';
|
||||||
|
const url = product.quantity || product.end_date
|
||||||
|
? `https://${product.site_url}/dashboard/${product.name.split('%%%')[0]}?token=${token}`
|
||||||
|
: `https://${product.site_url}`;
|
||||||
|
window.location.href = url;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{product.end_date || product.quantity ? 'KUNJUNGI SITUS' : 'PELAJARI LEBIH LANJUT'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<button className={`${styles.button} ${styles.checkoutButton}`} onClick={onCheckout}>
|
<button className={`${styles.button} ${styles.checkoutButton}`} onClick={onCheckout}>
|
||||||
Checkout
|
{Array.isArray(subscriptions) &&
|
||||||
|
subscriptions.some(sub =>
|
||||||
|
sub.product_id === product.id || sub.product_parent_id === product.id
|
||||||
|
) && 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)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{child.name} — Rp {parseInt(child.price || 0).toLocaleString('id-ID')}
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<div> {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}>
|
||||||
@@ -202,90 +311,65 @@ if (hasMatchingSubscription) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Subscription selector */}
|
||||||
{showSubscriptionSelector && !showNamingInput && (
|
{showSubscriptionSelector && !showNamingInput && (
|
||||||
<div className={styles.childSelector}>
|
<div className={styles.childSelector}>
|
||||||
<h5>Perpanjang {product.name}</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' }}>
|
||||||
{sub.product_name}
|
<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 === product.id}
|
|
||||||
onChange={() => setSelectedSubscriptionId(product.id)}
|
|
||||||
/>
|
|
||||||
Buat {product.name} 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} Baru</h5>
|
<h5>Buat {product.name.split('%%%')[0]} Baru</h5>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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);
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Container } from 'react-bootstrap';
|
import { Container } from 'react-bootstrap';
|
||||||
import styles from './Styles.module.css';
|
import styles from './ProductSection.module.css';
|
||||||
|
import CoverflowCarousel from './CoverflowCarousel';
|
||||||
|
import processProducts from '../helper/processProducts';
|
||||||
|
import shared from './Styles.module.css';
|
||||||
|
import useInView from '../hooks/useInView';
|
||||||
|
|
||||||
|
const ProductSection = ({ setSelectedProduct, setShowedModal, productSectionRef, setWillDo }) => {
|
||||||
const ProductSection = ({ hoveredCard, setHoveredCard, setSelectedProduct, setShowedModal, productSectionRef }) => {
|
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
|
const [filteredProducts, setFilteredProducts] = useState([]);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
|
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -16,88 +21,85 @@ useEffect(() => {
|
|||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const parentMap = {};
|
const processed = processProducts(data);
|
||||||
const childrenMap = {};
|
setProducts(processed);
|
||||||
|
setFilteredProducts(processed);
|
||||||
// Pisahkan parent dan child
|
|
||||||
data.forEach(product => {
|
|
||||||
if (product.sub_product_of) {
|
|
||||||
const parentId = product.sub_product_of;
|
|
||||||
if (!childrenMap[parentId]) childrenMap[parentId] = [];
|
|
||||||
childrenMap[parentId].push(product);
|
|
||||||
} else {
|
|
||||||
parentMap[product.id] = {
|
|
||||||
...product,
|
|
||||||
children: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pasang children ke parent
|
|
||||||
Object.keys(childrenMap).forEach(parentId => {
|
|
||||||
const parent = parentMap[parentId];
|
|
||||||
if (parent) {
|
|
||||||
parent.children = childrenMap[parentId];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ambil parent saja
|
|
||||||
const enrichedData = Object.values(parentMap);
|
|
||||||
|
|
||||||
setProducts(enrichedData);
|
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Fetch error:', err));
|
.catch(err => console.error('Fetch error:', err));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
// Extract unique categories from products
|
||||||
|
const categories = ['all', ...new Set(products.map(product => product.category).filter(Boolean))];
|
||||||
|
|
||||||
<section id="services" className="services pt-5">
|
// Filter products by category
|
||||||
<Container>
|
useEffect(() => {
|
||||||
<div className="section-heading text-center mb-4">
|
if (selectedCategory === 'all') {
|
||||||
<h4>OUR <em>PRODUCTS</em></h4>
|
setFilteredProducts(products);
|
||||||
<img src="/assets/images/heading-line-dec.png" alt="" />
|
} else {
|
||||||
<p>Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.</p>
|
setFilteredProducts(products.filter(product => product.category === selectedCategory));
|
||||||
</div>
|
}
|
||||||
<div className={styles.coursesGrid}>
|
}, [selectedCategory, products]);
|
||||||
{products &&
|
|
||||||
products[0]?.name &&
|
// Handle product selection for detail view
|
||||||
products
|
const handleViewDetail = (product, detailed) => {
|
||||||
.map(product => (
|
console.log(product, detailed)
|
||||||
<div
|
if(detailed) {
|
||||||
key={product.id}
|
window.location.href = product.site_landing_url;
|
||||||
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
|
return;
|
||||||
onClick={() => {
|
}
|
||||||
setSelectedProduct(product);
|
setSelectedProduct(product);
|
||||||
setShowedModal('product');
|
setShowedModal('product');
|
||||||
}}
|
setWillDo('checkout');
|
||||||
onMouseEnter={() => setHoveredCard(product.id)}
|
};
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
|
||||||
|
const { ref, inView } = useInView();
|
||||||
|
return (
|
||||||
|
<section id="products" style={{scrollMarginTop: '65px' }} className={`${styles.productSection} ${shared.revealSection} ${inView ? shared.isVisible : ''}`} ref={(el) => {
|
||||||
|
if (typeof productSectionRef === 'function') productSectionRef(el);
|
||||||
|
if (ref) ref.current = el;
|
||||||
|
}}>
|
||||||
|
<Container>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<h2 className={styles.sectionTitle}>Produk Unggulan</h2>
|
||||||
|
<p>Produk digital siap pakai untuk mempercepat pertumbuhan bisnis Anda.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
{categories.length > 2 && (
|
||||||
|
<div className={styles.filterContainer}>
|
||||||
|
<div className={styles.filterWrapper}>
|
||||||
|
<button
|
||||||
|
className={`${styles.filterBtn} ${selectedCategory === 'all' ? styles.active : ''}`}
|
||||||
|
onClick={() => setSelectedCategory('all')}
|
||||||
>
|
>
|
||||||
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
|
Semua Produk
|
||||||
{product.price === 0 && (
|
</button>
|
||||||
<span className={styles.courseLabel}>Free</span>
|
{categories.filter(cat => cat !== 'all').map(category => (
|
||||||
)}
|
<button
|
||||||
</div>
|
key={category}
|
||||||
<div className={styles.courseContent}>
|
className={`${styles.filterBtn} ${selectedCategory === category ? styles.active : ''}`}
|
||||||
<h3 className={styles.courseTitle}>{product.name}</h3>
|
onClick={() => setSelectedCategory(category)}
|
||||||
<p className={styles.courseDesc}>{product.description}</p>
|
|
||||||
<div className={styles.coursePrice}>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
product.price === 0
|
|
||||||
? styles.freePrice
|
|
||||||
: styles.currentPrice
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{product.price == null
|
{category}
|
||||||
? 'Pay-As-You-Go'
|
</button>
|
||||||
: `Rp ${product.price.toLocaleString('id-ID')}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Coverflow Carousel */}
|
||||||
|
<div className={styles.carouselContainer}>
|
||||||
|
{filteredProducts.length > 0 ? (
|
||||||
|
<CoverflowCarousel
|
||||||
|
products={filteredProducts}
|
||||||
|
onCardClick={handleViewDetail}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.noProducts}>
|
||||||
|
<p>Tidak ada produk yang tersedia saat ini.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
301
src/components/ProductSection.module.css
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
/* ProductSection.module.css */
|
||||||
|
|
||||||
|
.productSection {
|
||||||
|
padding: 36px 0; /* compact height */
|
||||||
|
background-color: #ffffff; /* solid white to blend with neighbors */
|
||||||
|
position: relative;
|
||||||
|
overflow: visible; /* allow mesh to bleed upward */
|
||||||
|
}
|
||||||
|
|
||||||
|
.productSection::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: -48px; bottom: -48px; /* keep mesh close to section */
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
/* Single Aurora Orb centered behind carousel */
|
||||||
|
background-image: radial-gradient(circle 920px at 50% 50%, rgba(0, 121, 222, 0.1), rgba(0, 121, 222, 0) 72%);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
transform-origin: center;
|
||||||
|
animation: auroraBreath 16s ease-in-out infinite alternate;
|
||||||
|
/* Soften both top and bottom edges to avoid straight cutoffs */
|
||||||
|
-webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12%, rgba(0,0,0,1) 88%, rgba(0,0,0,0) 100%);
|
||||||
|
mask-image: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12%, rgba(0,0,0,1) 88%, rgba(0,0,0,0) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copied from ServicesSection.module.css for consistency */
|
||||||
|
.sectionHeader {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px; /* tighter */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: clamp(1.6rem, 3.4vw, 2.0rem);
|
||||||
|
color: #111827; /* darker */
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headingLine { display: none; }
|
||||||
|
|
||||||
|
.sectionHeader p {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #4b5563;
|
||||||
|
max-width: 560px;
|
||||||
|
margin: 8px auto 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
/* End of copied styles */
|
||||||
|
|
||||||
|
/* Filter Styles */
|
||||||
|
.filterContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px; /* tighter */
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterBtn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #475569;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterBtn:hover {
|
||||||
|
border-color: #6a59ff;
|
||||||
|
color: #6a59ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterBtn.active {
|
||||||
|
background-color: #6a59ff;
|
||||||
|
border-color: #6a59ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Carousel Styles */
|
||||||
|
.carouselContainer {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 380px; /* compact */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.productSection::before {
|
||||||
|
background-image: radial-gradient(circle 720px at 50% 50%, rgba(0, 121, 222, 0.14), rgba(0, 121, 222, 0) 72%);
|
||||||
|
-webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12%, rgba(0,0,0,1) 88%, rgba(0,0,0,0) 100%);
|
||||||
|
mask-image: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12%, rgba(0,0,0,1) 88%, rgba(0,0,0,0) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes auroraBreath {
|
||||||
|
0% { transform: translate3d(0,0,0) scale(0.98); opacity: 0.9; }
|
||||||
|
50% { transform: translate3d(0,-4px,0) scale(1.04); opacity: 1; }
|
||||||
|
100% { transform: translate3d(0,0,0) scale(0.98); opacity: 0.92; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.noProducts {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 300px;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.productSection {
|
||||||
|
padding: 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterContainer {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselContainer {
|
||||||
|
min-height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noProducts {
|
||||||
|
height: 280px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.productSection {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader p {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterWrapper {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterBtn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselContainer {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noProducts {
|
||||||
|
height: 260px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.productSection {
|
||||||
|
padding: 35px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: clamp(1.4rem, 4vw, 1.8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterContainer {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterWrapper {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterBtn {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselContainer {
|
||||||
|
min-height: 370px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noProducts {
|
||||||
|
height: 240px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.productSection {
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: clamp(1.3rem, 5vw, 1.7rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterContainer {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterWrapper {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterBtn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselContainer {
|
||||||
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noProducts {
|
||||||
|
height: 220px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.productSection {
|
||||||
|
padding: 25px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: clamp(1.2rem, 6vw, 1.6rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterContainer {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterWrapper {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterBtn {
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselContainer {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noProducts {
|
||||||
|
height: 200px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,69 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Container, Row, Col, Card } from 'react-bootstrap';
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
|
import styles from './ServicesSection.module.css';
|
||||||
|
import { Network, Wrench, Code } from 'lucide-react';
|
||||||
|
import shared from './Styles.module.css';
|
||||||
|
import useInView from '../hooks/useInView';
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
icon: <Network size={28} />,
|
||||||
|
title: "Meshticon",
|
||||||
|
theme: "Network",
|
||||||
|
description: "Instalasi jaringan dan infrastruktur terstruktur yang memastikan konektivitas andal dan keamanan aset Anda, sehingga operasional bisnis berjalan mulus tanpa gangguan.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Wrench size={28} />,
|
||||||
|
title: "Techcare",
|
||||||
|
theme: "Support",
|
||||||
|
description: "Menyediakan layanan perakitan, servis, dan konsultasi perangkat keras untuk menjamin kinerja optimal dan penanganan masalah yang responsif, sehingga produktivitas tim Anda terjaga dan investasi teknologi Anda lebih awet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Code size={28} />,
|
||||||
|
title: "Gawechno",
|
||||||
|
theme: "Software",
|
||||||
|
description: "Pengembangan software, website, dan sistem otomasi kustom yang mengubah proses manual menjadi lebih efisien dan cerdas, memungkinkan Anda menekan biaya operasional dan membuka jalan bagi inovasi bisnis.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const ServicesSection = () => {
|
const ServicesSection = () => {
|
||||||
|
const { ref, inView } = useInView();
|
||||||
return (
|
return (
|
||||||
<section id="services" className="services py-5">
|
<section id="services" ref={ref} style={{scrollMarginTop: '65px'}} className={`${styles.blueprintContainer} ${shared.revealSection} ${inView ? shared.isVisible : ''}`}>
|
||||||
|
<div className={styles.blueprintGrid}></div>
|
||||||
|
<div className={styles.contentWrapper}>
|
||||||
<Container>
|
<Container>
|
||||||
<div className="section-heading text-center mb-4">
|
<div className={styles.sectionHeader}>
|
||||||
<h4>OUR <em>SERVICES</em></h4>
|
{/* Judul diubah */}
|
||||||
<img src="/assets/images/heading-line-dec.png" alt="" />
|
<h2 className={styles.sectionTitle}>Layanan Kami</h2>
|
||||||
<p>Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.</p>
|
<img src="/assets/images/heading-line-dec.png" alt="" className={styles.headingLine} />
|
||||||
|
<p>
|
||||||
|
Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Row className="gy-4 justify-content-center">
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<Col key={index} lg={4} md={6}>
|
||||||
|
<div className={styles.specCard}>
|
||||||
|
{/* Stuktur header disederhanakan */}
|
||||||
|
<div className={styles.cardHeader}>
|
||||||
|
<div className={styles.cardIcon}>{service.icon}</div>
|
||||||
|
<h3 className={styles.cardTitle}>{service.title}</h3>
|
||||||
|
<div className={styles.themePill}>
|
||||||
|
{/* Urutan dibalik: titik dulu, baru tulisan */}
|
||||||
|
<span className={styles.glowingDot}></span>
|
||||||
|
<span>{service.theme}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.cardBody}>
|
||||||
|
<p className={styles.cardDescription}>{service.description}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Row>
|
|
||||||
<Col lg={4}>
|
|
||||||
<Card className="mb-4">
|
|
||||||
<Card.Body>
|
|
||||||
<Card.Title>Mesthicon</Card.Title>
|
|
||||||
<Card.Text>Layanan instalasi jaringan, CCTV, dan infrastruktur teknologi.</Card.Text>
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col lg={4}>
|
|
||||||
<Card className="mb-4">
|
|
||||||
<Card.Body>
|
|
||||||
<Card.Title>Techcare</Card.Title>
|
|
||||||
<Card.Text>Perakitan komputer, servis, dan konsultasi infrastruktur digital.</Card.Text>
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col lg={4}>
|
|
||||||
<Card className="mb-4">
|
|
||||||
<Card.Body>
|
|
||||||
<Card.Title>Gawechno</Card.Title>
|
|
||||||
<Card.Text>Pembuatan software, website, sistem otomatisasi bisnis, dan aplikasi AI.</Card.Text>
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
</Col>
|
||||||
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
183
src/components/ServicesSection.module.css
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
/* === Step 1: Fading Blueprint Background (Exists) === */
|
||||||
|
.blueprintContainer {
|
||||||
|
padding: 60px 0; /* Dikurangi dari 80px */
|
||||||
|
position: relative;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blueprintGrid {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
/* --- Warna dan Ukuran Baru --- */
|
||||||
|
--grid-color-micro: #f4f8ff;
|
||||||
|
--grid-color-light: #dcecfc;
|
||||||
|
--grid-color-dark: #cddff2; /* Warna lebih muda dari sebelumnya (#b0c4de) */
|
||||||
|
|
||||||
|
background-image:
|
||||||
|
/* Micro grid (paling terang) */
|
||||||
|
linear-gradient(to right, var(--grid-color-micro) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, var(--grid-color-micro) 1px, transparent 1px),
|
||||||
|
/* Grid terang */
|
||||||
|
linear-gradient(to right, var(--grid-color-light) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, var(--grid-color-light) 1px, transparent 1px),
|
||||||
|
/* Grid gelap (utama) - Ditebalkan menjadi 2px */
|
||||||
|
linear-gradient(to right, var(--grid-color-dark) 2px, transparent 2px),
|
||||||
|
linear-gradient(to bottom, var(--grid-color-dark) 2px, transparent 2px);
|
||||||
|
|
||||||
|
/* Ukuran grid yang diperbarui */
|
||||||
|
background-size:
|
||||||
|
10px 10px, /* Ukuran micro grid */
|
||||||
|
10px 10px,
|
||||||
|
40px 40px, /* Ukuran grid terang */
|
||||||
|
40px 40px,
|
||||||
|
60px 60px, /* Ukuran grid utama (diperkecil dari 80px) */
|
||||||
|
60px 60px;
|
||||||
|
|
||||||
|
background-position: -1px -1px; /* Terapkan ke semua layer */
|
||||||
|
|
||||||
|
mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
|
||||||
|
-webkit-mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentWrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Step 2: Blueprint Spec Card Design === */
|
||||||
|
|
||||||
|
/* Original Section Header Styling */
|
||||||
|
.sectionHeader {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px; /* Dikurangi dari 50px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: clamp(1.8rem, 4vw, 2.2rem);
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader .headingLine {
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader p {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #475569;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* New Spec Card Styles */
|
||||||
|
.specCard {
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
-webkit-backdrop-filter: blur(5px);
|
||||||
|
border: 1px solid #9cb3d9; /* Blueprint line color */
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 380px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specCard:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
border-color: #0052cc;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 82, 204, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center; /* Membuat ikon, judul, dan pil sejajar */
|
||||||
|
gap: 12px; /* Jarak antar item */
|
||||||
|
padding: 16px; /* Dikurangi dari 20px */
|
||||||
|
border-bottom: 1px dashed #9cb3d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardIcon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #0052cc;
|
||||||
|
flex-shrink: 0; /* Mencegah ikon menyusut */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTitle {
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
flex-grow: 1; /* Mendorong pil ke kanan */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardBody {
|
||||||
|
padding: 16px; /* Dikurangi dari 20px */
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (CTA footer removed) */
|
||||||
|
|
||||||
|
.cardDescription {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #475569;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.themePill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
background-color: #eef6ff;
|
||||||
|
border: 1px solid #0052cc; /* Border dengan warna brand */
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #0052cc;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0; /* Mencegah pil menyusut */
|
||||||
|
}
|
||||||
|
|
||||||
|
.glowingDot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #0066ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: blink 2.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 7px 2px rgba(0, 82, 204, 0.6);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
box-shadow: 0 0 3px 1px rgba(0, 82, 204, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA link removed */
|
||||||
@@ -23,15 +23,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background-color: white;
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
padding: 14px 6rem;
|
padding: 14px 6rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
background: transparent; /* blend with top page */
|
||||||
position: sticky;
|
-webkit-backdrop-filter: saturate(140%) blur(14px);
|
||||||
top: 0;
|
backdrop-filter: saturate(140%) blur(14px);
|
||||||
z-index: 1000;
|
border-bottom: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerScrolled {
|
||||||
|
background: rgba(255, 255, 255, 0.65);
|
||||||
|
border-bottom: 1px solid rgba(203, 213, 225, 0.8);
|
||||||
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports not ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) {
|
||||||
|
.header { /* revert: no backdrop effects */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
@@ -83,12 +96,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loginButton {
|
.loginButton {
|
||||||
background-color: transparent;
|
background-color: #2563eb; /* brand color */
|
||||||
color: #64748b;
|
color: #ffffff;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 10px; /* rounded corner */
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton:hover {
|
||||||
|
background-color: #1e40af;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hero Section */
|
/* Hero Section */
|
||||||
@@ -175,6 +195,53 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionEyebrow {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #1e40af;
|
||||||
|
background: #e8f0ff;
|
||||||
|
border: 1px solid #c7d2fe;
|
||||||
|
border-radius: 999px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin: 4px 0 6px 0;
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.sectionTitle { font-size: 1.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionRule {
|
||||||
|
width: 72px;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%);
|
||||||
|
border-radius: 999px;
|
||||||
|
margin: 8px 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionSubtitle {
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.coursesTitle {
|
.coursesTitle {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -184,24 +251,32 @@
|
|||||||
|
|
||||||
.coursesGrid {
|
.coursesGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: 1fr;
|
||||||
gap: 2rem;
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.coursesGrid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.coursesGrid { grid-template-columns: repeat(3, 1fr); }
|
||||||
}
|
}
|
||||||
.courseCard {
|
.courseCard {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 1rem;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.3);
|
border: 1px solid #e5e7eb; /* thin border, no base shadow */
|
||||||
transition: all 0.3s ease;
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
/* Tambahan untuk menghilangkan highlight biru */
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.courseCard:focus {
|
.courseCard:focus {
|
||||||
@@ -210,14 +285,15 @@
|
|||||||
|
|
||||||
|
|
||||||
.courseCardHover {
|
.courseCardHover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
border-color: #d1d5db;
|
||||||
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.courseImage {
|
.courseImage {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 160px;
|
||||||
background-color: #e2e8f0;
|
background-color: white;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -241,11 +317,33 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.courseContent {
|
.courseContentTop {
|
||||||
padding: 1.5rem;
|
padding: 0.75rem 0.75rem 0rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.titleRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.courseContentTop p {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.courseContentBottom {
|
||||||
|
padding: 0.75rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.courseCategory {
|
.courseCategory {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
@@ -255,26 +353,59 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.courseTitle {
|
.courseTitle {
|
||||||
font-size: 1.2rem;
|
flex: 1 1 auto;
|
||||||
font-weight: bold;
|
min-width: 0; /* allow ellipsis shrink in flex */
|
||||||
color: #1e293b;
|
font-size: 1rem;
|
||||||
margin-bottom: 1rem;
|
font-weight: 700;
|
||||||
margin-top: 0;
|
color: #0f172a;
|
||||||
|
margin: 0; /* in-row with pills */
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.courseStats {
|
.pillRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
gap: 0.4rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 1rem;
|
justify-content: flex-end;
|
||||||
font-size: 0.9rem;
|
flex: 0 0 auto;
|
||||||
color: #64748b;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.courseStat {
|
.pill {
|
||||||
display: flex;
|
padding: 2px 8px;
|
||||||
align-items: center;
|
background-color: #f3f4f6;
|
||||||
gap: 0.25rem;
|
color: #374151;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.courseTitle { font-size: 0.95rem; }
|
||||||
|
.pill { font-size: 10px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillModules {
|
||||||
|
background-color: #e8f0ff;
|
||||||
|
border-color: #c7d2fe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillSessions {
|
||||||
|
background-color: #ecfdf5;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleSeparator {
|
||||||
|
border-top: 1px dashed #e5e7eb;
|
||||||
|
margin: 0.5rem 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coursePrice {
|
.coursePrice {
|
||||||
@@ -296,11 +427,43 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.freePrice {
|
.freePrice {
|
||||||
font-size: 1.2rem;
|
font-size: 1.05rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #059669;
|
color: #059669;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compact description */
|
||||||
|
.courseDesc {
|
||||||
|
margin: 0.25rem 0 0.25rem;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enrollButton {
|
||||||
|
background-color: #2563eb;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
padding: 0.55rem 0.9rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, transform 0.05s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enrollButton:hover {
|
||||||
|
background-color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enrollButton:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
.featuresContainer {
|
.featuresContainer {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -329,6 +492,28 @@
|
|||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Spacing for Academy section to avoid tight gap with FAQ */
|
||||||
|
.academySection {
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.academySection { padding-bottom: 2rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reveal on scroll/load */
|
||||||
|
.revealSection {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px);
|
||||||
|
transition: opacity 480ms ease-out, transform 480ms ease-out;
|
||||||
|
will-change: opacity, transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isVisible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
.featureItem {
|
.featureItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
@@ -476,44 +661,44 @@
|
|||||||
/* Responsive Styles */
|
/* Responsive Styles */
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.modalBody {
|
.modalBody {
|
||||||
width: 80%;
|
width: 85%;
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 0 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
padding: 14px 2rem;
|
padding: 14px 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heroContainer {
|
.heroContainer {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctaTitle,
|
.ctaTitle,
|
||||||
.courseTitle {
|
.courseTitle {
|
||||||
font-size: 15px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctaDescription,
|
.ctaDescription,
|
||||||
.courseContent p {
|
.courseContentTop, .courseContentBottom p {
|
||||||
font-size: 13px;
|
|
||||||
margin: 6px 0px;
|
margin: 6px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctaCard,
|
.ctaCard,
|
||||||
.Section {
|
.Section {
|
||||||
padding: 2rem 0.8rem;
|
padding: 2rem 1rem;
|
||||||
background-color: #f8fafc;
|
background-color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.courseContent {
|
.courseContentTop, .courseContentBottom {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0.8rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctaContainer,
|
.ctaContainer,
|
||||||
.coursesGrid {
|
.coursesGrid {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(173px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,6 +732,7 @@
|
|||||||
display: none;
|
display: none;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileMenu {
|
.mobileMenu {
|
||||||
@@ -554,7 +740,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Tampilkan burger dan menu di mobile */
|
/* Tampilkan burger dan menu di mobile */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 768px) {
|
||||||
.nav {
|
.nav {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -575,11 +761,12 @@
|
|||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
z-index: 10;
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileMenu button {
|
.mobileMenu button {
|
||||||
@@ -589,6 +776,8 @@
|
|||||||
color: white;
|
color: white;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileMenu button:hover {
|
.mobileMenu button:hover {
|
||||||
@@ -600,6 +789,108 @@
|
|||||||
color: #2563eb;
|
color: #2563eb;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logoutButton {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid #2563eb;
|
||||||
|
color: #2563eb;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoutButton:hover {
|
||||||
|
background-color: #2563eb;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.header {
|
||||||
|
padding: 12px 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Section {
|
||||||
|
padding: 1.5rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coursesGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaContainer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featuresList {
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureItem {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureIcon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureTitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileMenu {
|
||||||
|
right: 15px;
|
||||||
|
top: 55px;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileMenu button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.Section {
|
||||||
|
padding: 1.2rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureItem {
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureIcon {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureTitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureDescription {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileMenu {
|
||||||
|
right: 10px;
|
||||||
|
top: 50px;
|
||||||
|
min-width: 130px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileMenu button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loggedInContainer {
|
.loggedInContainer {
|
||||||
@@ -627,3 +918,171 @@
|
|||||||
background-color: #2563eb;
|
background-color: #2563eb;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clientLogoWrapper {
|
||||||
|
max-width: 150px; /* batas lebar */
|
||||||
|
max-height: 80px; /* batas tinggi */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clientLogo {
|
||||||
|
max-height: 80px;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer{
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline; /* opsional kalau mau hover effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
max-width: 150px; /* biar logo tidak terlalu besar */
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|||||||
29
src/components/TestAnimation.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const TestAnimation = () => {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
scale: count % 2 === 0 ? 1 : 1.5,
|
||||||
|
rotate: count * 90
|
||||||
|
}}
|
||||||
|
transition={{ duration: 1 }}
|
||||||
|
style={{
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
backgroundColor: 'blue',
|
||||||
|
margin: '0 auto'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button onClick={() => setCount(count + 1)}>
|
||||||
|
Animate {count}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestAnimation;
|
||||||
@@ -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,22 +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 }) => {
|
const Dashboard = ({
|
||||||
const [postLoginAction, setPostLoginAction] = useState(null);
|
subscriptions,
|
||||||
const [selectedProduct, setSelectedProduct] = useState({});
|
setSelectedProduct,
|
||||||
const [hoveredCard, setHoveredCard] = useState(null);
|
setShowedModal,
|
||||||
const [showedModal, setShowedModal] = useState(null);
|
userData,
|
||||||
|
setWillDo
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [activeTab, setActiveTab] = useState("products");
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
|
const [hoveredCard, setHoveredCard] = useState(null);
|
||||||
|
|
||||||
|
const [settings, setSettings] = useState({
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
profile_data: {
|
||||||
|
name: "",
|
||||||
|
image: "",
|
||||||
|
phone: "",
|
||||||
|
address: "",
|
||||||
|
company: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [purchaseHistory, setPurchaseHistory] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!subscriptions) return;
|
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
||||||
|
const token = match ? match[2] : null;
|
||||||
|
|
||||||
// Step 1: Group subscriptions by product_name
|
if (!token) {
|
||||||
function groupSubscriptionsByProductName(subs) {
|
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]) {
|
||||||
@@ -29,253 +89,353 @@ const CoursePage = ({ subscriptions }) => {
|
|||||||
subscriptions: []
|
subscriptions: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update end_date jika lebih baru
|
|
||||||
const currentEnd = new Date(result[name].end_date);
|
const currentEnd = new Date(result[name].end_date);
|
||||||
const thisEnd = new Date(sub.end_date);
|
const thisEnd = new Date(sub.end_date);
|
||||||
if (thisEnd > currentEnd) {
|
if (thisEnd > currentEnd) result[name].end_date = sub.end_date;
|
||||||
result[name].end_date = sub.end_date;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tambahkan quantity jika unit_type adalah 'token'
|
if (sub.unit_type === "token") {
|
||||||
if (sub.unit_type == 'token') {
|
|
||||||
result[name].quantity += sub.quantity ?? 0;
|
result[name].quantity += sub.quantity ?? 0;
|
||||||
} else {
|
} else {
|
||||||
result[name].quantity += 1; // Bisa diabaikan atau tetap hitung 1 per subscription
|
result[name].quantity += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
result[name].subscriptions.push(sub);
|
result[name].subscriptions.push(sub);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!subscriptions) return;
|
||||||
|
|
||||||
const groupedSubs = groupSubscriptionsByProductName(subscriptions);
|
const groupedSubs = groupSubscriptionsByProductName(subscriptions);
|
||||||
|
const productIds = [...new Set(subscriptions.map((s) => s.product_id))];
|
||||||
|
|
||||||
// Step 2: Ambil semua unique product_id (tetap diperlukan untuk ambil metadata dari API)
|
fetch("https://bot.kediritechnopark.com/webhook/store-production/products", {
|
||||||
const productIds = [...new Set(subscriptions.map(s => s.product_id))];
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
// Step 3: Fetch product metadata
|
body: JSON.stringify({ itemsId: productIds, withChildren: true })
|
||||||
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
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)) // ✅ hanya produk yang ada di metadata
|
data.forEach((item) => {
|
||||||
.map(group => {
|
dataMap[item.id] = { ...item, children: [] };
|
||||||
const productData = data.find(p => p.id == group.product_id);
|
});
|
||||||
|
|
||||||
// Cek fallback image dari parent jika image kosong dan sub_product_of ada
|
data.forEach((item) => {
|
||||||
let image = productData?.image || '';
|
if (item.sub_product_of && dataMap[item.sub_product_of]) {
|
||||||
let description = productData?.description || '';
|
dataMap[item.sub_product_of].children.push(dataMap[item.id]);
|
||||||
if (!image && productData?.sub_product_of) {
|
}
|
||||||
const parent = data.find(p => p.id === productData.sub_product_of);
|
});
|
||||||
image = parent?.image || '';
|
|
||||||
description = parent?.description || '';
|
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,
|
||||||
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);
|
||||||
console.log('Enriched Data:', enrichedData);
|
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Fetch error:', err));
|
.catch((err) => console.error("Fetch error:", err));
|
||||||
}, [subscriptions]);
|
}, [subscriptions]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const features = [
|
|
||||||
{
|
|
||||||
icon: '🌐',
|
|
||||||
title: 'Belajar Langsung dari Mentor Terbaik',
|
|
||||||
description:
|
|
||||||
'Kursus kami dirancang dan dipandu oleh para praktisi, pengajar, dan mentor yang ahli di bidangnya—mulai dari bisnis digital, teknologi, desain, hingga kecerdasan buatan. Semua materi disemakan dengan bahasa yang sederhana, mudah dipahami, dan langsung bisa dipraktikkan.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '⏰',
|
|
||||||
title: 'Fleksibel Sesuai Gaya Hidupmu',
|
|
||||||
description:
|
|
||||||
'Sibuk kerja? Urus anak? Atau lagi nyantai belajar Teknilog, di Akademi ini kamu bisa belajar kapan saja di mana saja, tanpa terikat waktu. Semua kursus kami bisa diakses ulang dan kamu bebas atur ritme belajar mu sendiri. Bebas lekukan, makamali ngatif.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '⚡',
|
|
||||||
title: 'Belajar Cepat, Dampak Nyata',
|
|
||||||
description:
|
|
||||||
'Kami percaya proses belajar tidak harus lama lama! Dengan pendekatan yang tepat, kamu bisa menguasai keterampilan baru hanya dalam hitungan minggu—buken bulan! Mulai dari belajar desain, digital marketing, AI, hingga manajemen usaha, semua bisa kamu kuasai dengan cepat dan tepat guna.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ fontFamily: 'Inter, system-ui, sans-serif' }}>
|
<Container fluid className="py-4 px-3 px-md-5">
|
||||||
|
<Tabs activeKey={activeTab} onSelect={(k) => setActiveTab(k)} className="mb-3">
|
||||||
{/* Courses Section */}
|
<Tab eventKey="products" title="Produk Saya">
|
||||||
<section className={styles.Section}>
|
<Row>
|
||||||
<div className={styles.coursesContainer}>
|
{products.map((product, i) => (
|
||||||
<h2 className={styles.coursesTitle}>MY PRODUCTS</h2>
|
<Col md={4} key={i} className="mb-4">
|
||||||
<div className={styles.coursesGrid}>
|
<Card
|
||||||
{products &&
|
className={`h-100 shadow-sm p-2 ${hoveredCard === product.name ? "border-primary" : ""}`}
|
||||||
products[0]?.name &&
|
|
||||||
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 className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
|
|
||||||
{/* {product.price == 0 && (
|
|
||||||
<span className={styles.courseLabel}>Free</span>
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
<div className={styles.courseContent}>
|
|
||||||
<h3 className={styles.courseTitle}>{product.name}</h3>
|
|
||||||
<p className={styles.courseDesc}>{product.description}</p>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Features Section */}
|
|
||||||
<section className={styles.Section}>
|
|
||||||
<div className={styles.featuresContainer}>
|
|
||||||
<h2 className={styles.featuresTitle}>Mengapa Memilih Akademi Kami?</h2>
|
|
||||||
<p className={styles.featuresDescription}>
|
|
||||||
Di era digital yang terus berubah, Akademi kami hadir sebagai ruang tumbuh untuk siapa saja yang ingin berkembang.
|
|
||||||
Baik pelajar, profesional, UMKM, hingga pemula teknologi—kami bantu kamu naik level dengan materi praktis,
|
|
||||||
akses mudah, dan komunitas suportif.
|
|
||||||
</p>
|
|
||||||
<div className={styles.featuresList}>
|
|
||||||
{features.map((feature, index) => (
|
|
||||||
<div key={index} className={styles.featureItem}>
|
|
||||||
<div className={styles.featureIcon}>{feature.icon}</div>
|
|
||||||
<div className={styles.featureContent}>
|
|
||||||
<h3 className={styles.featureTitle}>{feature.title}</h3>
|
|
||||||
<p className={styles.featureDescription}>{feature.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* CTA Section */}
|
|
||||||
<section className={styles.Section}>
|
|
||||||
<div className={styles.ctaContainer}>
|
|
||||||
<div className={styles.ctaCard}>
|
|
||||||
<div>
|
|
||||||
<div className={styles.ctaIcon}>😊</div>
|
|
||||||
<h3 className={styles.ctaTitle}>Murid Daftar Disini</h3>
|
|
||||||
<p className={styles.ctaDescription}>
|
|
||||||
Ambil langkah pertama menuju karier impian atau hobi barumu bersama Akademi Kami.
|
|
||||||
Belajar dengan cara yang menyenangkan, fleksibel, dan penuh manfaat.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button className={styles.ctaButton}>START LEARNING</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.ctaCard}>
|
|
||||||
<div>
|
|
||||||
<div className={styles.ctaIcon}>👨🏫</div>
|
|
||||||
<h3 className={styles.ctaTitle}>Guru Daftar Disini</h3>
|
|
||||||
<p className={styles.ctaDescription}>
|
|
||||||
Ajarkan apa yang kamu cintai. Akademi kami memberikan semua alat
|
|
||||||
dan dukungan yang kamu butuhkan untuk membuat kursusmu sendiri.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button className={styles.ctaButton}>START TEACHING</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<footer className={styles.footer}>
|
|
||||||
<div className={styles.footerContent}>
|
|
||||||
<p className={styles.footerText}>Created by Academy Kediri Techno Park</p>
|
|
||||||
<div className={styles.socialLinks}>
|
|
||||||
<a href="#" className={styles.socialLink}>📷</a>
|
|
||||||
<a href="#" className={styles.socialLink}>📱</a>
|
|
||||||
<a href="#" className={styles.socialLink}>📧</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
{/* Unified Modal */}
|
|
||||||
{showedModal && (
|
|
||||||
<div
|
|
||||||
className={styles.modal}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowedModal(null);
|
setSelectedProduct(product);
|
||||||
setSelectedProduct({});
|
setShowedModal("product");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<Card.Body>
|
||||||
className={styles.modalBody}
|
<Card.Title>
|
||||||
onClick={(e) => e.stopPropagation()}
|
📦 {product.name.split("%%%")[0] + ' | ' + product.realProductName}
|
||||||
>
|
</Card.Title>
|
||||||
{showedModal === 'product' && (
|
<Card.Text style={{ fontSize: "0.9rem", color: "#555" }}>
|
||||||
<ProductDetailPage
|
{product.description}
|
||||||
setPostLoginAction={setPostLoginAction}
|
</Card.Text>
|
||||||
setShowedModal={setShowedModal}
|
</Card.Body>
|
||||||
product={selectedProduct}
|
<Card.Footer className="d-flex justify-content-between align-items-center">
|
||||||
onClose={() => {
|
<small className="text-muted">
|
||||||
setShowedModal(null);
|
{product.unit_type === "duration"
|
||||||
setSelectedProduct({});
|
? `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>
|
||||||
{showedModal === 'login' && (
|
</Col>
|
||||||
<Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} />
|
<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>
|
||||||
)}
|
<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;
|
||||||
|
|||||||
30
src/helper/processProducts.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// processProducts.js
|
||||||
|
export default function processProducts(data) {
|
||||||
|
const parentMap = {};
|
||||||
|
const childrenMap = {};
|
||||||
|
|
||||||
|
// Pisahkan parent dan child
|
||||||
|
data.forEach(product => {
|
||||||
|
if (product.sub_product_of) {
|
||||||
|
const parentId = product.sub_product_of;
|
||||||
|
if (!childrenMap[parentId]) childrenMap[parentId] = [];
|
||||||
|
childrenMap[parentId].push(product);
|
||||||
|
} else {
|
||||||
|
parentMap[product.id] = {
|
||||||
|
...product,
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pasang children ke parent
|
||||||
|
Object.keys(childrenMap).forEach(parentId => {
|
||||||
|
const parent = parentMap[parentId];
|
||||||
|
if (parent) {
|
||||||
|
parent.children = childrenMap[parentId];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ambil parent saja
|
||||||
|
return Object.values(parentMap);
|
||||||
|
}
|
||||||
14
src/hooks/useInView.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
// Simplified: reveal once on initial load only (no scroll-based triggers)
|
||||||
|
export default function useInView() {
|
||||||
|
const ref = useRef(null);
|
||||||
|
const [inView, setInView] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = requestAnimationFrame(() => setInView(true));
|
||||||
|
return () => cancelAnimationFrame(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { ref, inView };
|
||||||
|
}
|
||||||
@@ -1,3 +1,27 @@
|
|||||||
|
:root {
|
||||||
|
--brand: #0057b8ff; /* primary brand color */
|
||||||
|
--brand-600: #004a9e; /* hover */
|
||||||
|
--brand-700: #003e85; /* active */
|
||||||
|
/* Hero and surface tokens */
|
||||||
|
--surface: #ffffff;
|
||||||
|
--text: #0f172a; /* slate-900 */
|
||||||
|
--muted: #475569; /* slate-600 */
|
||||||
|
--radius-2xl: 20px;
|
||||||
|
--shadow-soft: 0 20px 50px rgba(15, 23, 42, 0.08);
|
||||||
|
--header-h: 84px; /* approximate header height for hero sizing */
|
||||||
|
/* Neutral shadows for CTAs */
|
||||||
|
--shadow-neutral-s: 0 6px 16px rgba(17, 24, 39, 0.08);
|
||||||
|
--shadow-neutral-m: 0 8px 22px rgba(17, 24, 39, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
:root { --header-h: 76px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
:root { --header-h: 70px; }
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
|||||||