This commit is contained in:
Vassshhh
2025-07-30 20:31:37 +07:00
parent ee28551344
commit 731e7d90cc
11 changed files with 1347 additions and 100 deletions

54
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"react": "^19.1.1", "react": "^19.1.1",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-dom": "^19.1.1", "react-dom": "^19.1.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",
"slick-carousel": "^1.8.1", "slick-carousel": "^1.8.1",
@@ -14163,6 +14164,53 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz",
"integrity": "sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.1.tgz",
"integrity": "sha512-bavdk2BA5r3MYalGKZ01u8PGuDBloQmzpBZVhDLrOOv1N943Wq6dcM9GhB3x8b7AbqPMEezauv4PeGkAJfy7FQ==",
"license": "MIT",
"dependencies": {
"react-router": "7.7.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-router/node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/react-scripts": { "node_modules/react-scripts": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -15108,6 +15156,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",

View File

@@ -11,6 +11,7 @@
"react": "^19.1.1", "react": "^19.1.1",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-dom": "^19.1.1", "react-dom": "^19.1.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",
"slick-carousel": "^1.8.1", "slick-carousel": "^1.8.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// import './assets/css/templatemo-chain-app-dev.css'; // Assuming you copy your original CSS here import styles from './components/Styles.module.css';
// import './assets/css/animated.css';
// import './assets/css/owl.css'; // Import components
import Login from './components/Login';
// Import your converted React components
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';
@@ -15,14 +15,70 @@ import KnowledgeBaseSection from './components/KnowledgeBaseSection';
import ClientsSection from './components/ClientsSection'; import ClientsSection from './components/ClientsSection';
import Footer from './components/Footer'; import Footer from './components/Footer';
import ProductDetailPage from './components/ProductDetailPage';
function HomePage({
hoveredCard,
setHoveredCard,
selectedProduct,
setSelectedProduct,
showedModal,
setShowedModal,
productSectionRef,
courseSectionRef
}) {
return (
<>
<HeroSection />
<ServicesSection />
<ProductSection
productSectionRef={productSectionRef}
hoveredCard={hoveredCard}
setHoveredCard={setHoveredCard}
setSelectedProduct={setSelectedProduct}
setShowedModal={setShowedModal}
/>
<AcademySection
courseSectionRef={courseSectionRef}
hoveredCard={hoveredCard}
setHoveredCard={setHoveredCard}
setSelectedProduct={setSelectedProduct}
setShowedModal={setShowedModal} />
<AboutUsSection />
<KnowledgeBaseSection />
<ClientsSection />
</>
);
}
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 [selectedProduct, setSelectedProduct] = useState({});
const [showedModal, setShowedModal] = useState(null); // 'product' | 'login' | null
const [postLoginAction, setPostLoginAction] = useState(null);
const [username, setUsername] = useState(null);
const productSectionRef = useRef(null);
const courseSectionRef = useRef(null);
const scrollToProduct = () => {
productSectionRef.current?.scrollIntoView({ behavior: "smooth" });
};
const scrollToCourse = () => {
courseSectionRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => { useEffect(() => {
// Simulate preloader and remove it after some time
const timer = setTimeout(() => { const timer = setTimeout(() => {
setLoading(false); setLoading(false);
}, 1000); // Adjust time as needed }, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, []);
@@ -42,28 +98,59 @@ function App() {
} }
return ( return (
<Router>
<div className="App"> <div className="App">
<Header /> <Header username={username} scrollToProduct={scrollToProduct} scrollToCourse={scrollToCourse} setShowedModal={setShowedModal} />
<HeroSection /> <Routes>
{/* FULL WIDTH IMAGE SECTION */} <Route
{/* This can be a separate component or integrated into HeroSection */} path="/"
<div className="custom-image-section wow fadeInRight"> element={
<a href="https://registration.kediritechnopark.com/" target="_blank" className="custom-image-link" rel="noopener noreferrer"> <HomePage
<div className="custom-image-wrapper"> hoveredCard={hoveredCard}
<img src="/assets/images/FREE!.png" className="custom-image" /> setHoveredCard={setHoveredCard}
<div className="light-glare"></div> selectedProduct={selectedProduct}
</div> setSelectedProduct={setSelectedProduct}
</a> showedModal={showedModal}
</div> setShowedModal={setShowedModal}
productSectionRef={productSectionRef}
<ServicesSection /> courseSectionRef={courseSectionRef}
<ProductSection /> />
<AcademySection /> }
<AboutUsSection /> />
<KnowledgeBaseSection /> </Routes>
<ClientsSection />
<Footer /> <Footer />
{/* Unified Modal */}
{showedModal && (
<div
className={styles.modal}
onClick={() => {
setShowedModal(null);
setSelectedProduct({});
}}
>
<div
className={styles.modalBody}
onClick={(e) => e.stopPropagation()}
>
{showedModal === 'product' && (
<ProductDetailPage
setPostLoginAction={setPostLoginAction}
setShowedModal={setShowedModal}
product={selectedProduct}
onClose={() => {
setShowedModal(null);
setSelectedProduct({});
}}
/>
)}
{showedModal === 'login' && (
<Login postLoginAction={postLoginAction} setPostLoginAction={setPostLoginAction} onClose={() => setShowedModal(null)} />
)}
</div> </div>
</div>
)}
</div>
</Router>
); );
} }

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Container, Row, Col, Card, Button } from 'react-bootstrap'; import { Container } from 'react-bootstrap';
import styles from './Styles.module.css';
const AcademySection = () => { const AcademySection = ({hoveredCard, setHoveredCard, setSelectedProduct, setShowedModal, courseSectionRef}) => {
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
useEffect(() => { useEffect(() => {
@@ -18,43 +19,53 @@ const AcademySection = () => {
}, []); }, []);
return ( return (
<section id="academy" className="academy-tables py-5">
<section id="services" className="services py-5">
<Container> <Container>
<div className="section-heading text-center mb-4"> <div className="section-heading text-center mb-4">
<h4>OUR <em>ACADEMY PROGRAM</em></h4> <h4>OUR <em>ACADEMY PROGRAM</em></h4>
<img src="/assets/images/heading-line-dec.png" alt="" /> <img src="/assets/images/heading-line-dec.png" alt="" />
<p>Academy Program adalah wadah belajar digital untuk anak-anak dan remaja. Didesain interaktif, kreatif, dan gratis setiap modul membekali peserta dengan keterampilan masa depan, dari teknologi dasar hingga coding dan proyek nyata.</p> <p>Academy Program adalah wadah belajar digital untuk anak-anak dan remaja. Didesain interaktif, kreatif, dan gratis setiap modul membekali peserta dengan keterampilan masa depan, dari teknologi dasar hingga coding dan proyek nyata.</p>
</div> </div>
<Row> <div className={styles.coursesGrid}>
{products.map((product, idx) => ( {products &&
<Col lg={3} md={4} sm={6} xs={12} key={idx} className="mb-4"> products[0]?.name &&
<Card className="academy-item-regular h-100"> products.map(product => (
<Card.Body> <div
<Card.Title>{product.name}</Card.Title> key={product.id}
<div className="icon mb-3"> className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
<img src={product.image || '/assets/images/pricing-table-01.png'} alt={product.name} className="img-fluid" /> onClick={() => {
</div> setSelectedProduct(product);
<ul> setShowedModal('product');
{product.duration && ( }}
<li> onMouseEnter={() => setHoveredCard(product.id)}
Durasi:{" "} onMouseLeave={() => setHoveredCard(null)}
{product.duration.hours >
? `${product.duration.hours} jam` <div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
: product.duration.days {product.price === 0 && (
? `${product.duration.days} hari` <span className={styles.courseLabel}>Free</span>
: "-"}
</li>
)} )}
<li>Harga: {product.currency} {product.price.toLocaleString()}</li>
</ul>
<div className="border-button mt-3">
<Button variant="outline-primary" href="#program">Lihat Detail</Button>
</div> </div>
</Card.Body> <div className={styles.courseContent}>
</Card> <h3 className={styles.courseTitle}>{product.name}</h3>
</Col> <p className={styles.courseDesc}>{product.description}</p>
<div className={styles.coursePrice}>
<span
className={
product.price === 0
? styles.freePrice
: styles.currentPrice
}
>
{product.price == null
? 'Pay-As-You-Go'
: `Rp ${product.price.toLocaleString('id-ID')}`}
</span>
</div>
</div>
</div>
))} ))}
</Row> </div>
</Container> </Container>
</section> </section>
); );

View File

@@ -1,24 +1,72 @@
import React from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Navbar, Nav, Container } from 'react-bootstrap'; import { useNavigate } from 'react-router-dom';
import { Navbar, Nav, Container } from 'react-bootstrap';
import styles from './Styles.module.css';
const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal }) => {
const navigate = useNavigate();
const [hoveredNav, setHoveredNav] = useState(null);
const Header = () => {
return ( return (
<Navbar bg="light" expand="lg" sticky="top">
<Container> <header className={styles.header}>
<Navbar.Brand href="#">Kediri Technopark</Navbar.Brand> <img src="./kediri-technopark-logo.png" className={styles.logo} alt="Logo" />
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav"> <nav className={styles.nav}>
<Nav className="me-auto">
<Nav.Link href="#top">Home</Nav.Link> <a
<Nav.Link href="#services">Services</Nav.Link> className={`${styles.navLink} ${hoveredNav === 2 ? styles.navLinkHover : ''}`}
<Nav.Link href="#produk">Product</Nav.Link> onMouseEnter={() => setHoveredNav(2)}
<Nav.Link href="#academy">Academy</Nav.Link> onMouseLeave={() => setHoveredNav(null)}
<Nav.Link href="#about">About</Nav.Link> onClick={() => {
<Nav.Link href="#knowledge">Knowledge</Nav.Link> if (username == null) {
</Nav> scrollToProduct();
</Navbar.Collapse> }
</Container> else {
</Navbar> navigate('/products');
}
}}
>
PRODUCTS
</a>
<a
className={`${styles.navLink} ${hoveredNav === 3 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(3)}
onMouseLeave={() => setHoveredNav(null)}
onClick={() => {
if (username == null) {
scrollToCourse();
}
else {
navigate('/courses');
}
}}
>
COURSES
</a>
<a
className={`${styles.navLink} ${hoveredNav === 4 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(4)}
onMouseLeave={() => setHoveredNav(null)}
>
USER
</a>
</nav>
<div className={styles.authButtons}>
{username ? (
<span style={{ color: '#2563eb', fontWeight: '600' }}>
Halo, {username}
</span>
) : (
<button className={styles.loginButton} onClick={() => setShowedModal('login')}>
LOGIN
</button>
)}
</div>
</header>
); );
}; };

240
src/components/Login.js Normal file
View File

@@ -0,0 +1,240 @@
import React, { useState } from 'react';
const LoginRegister = ({postLoginAction, setPostLoginAction}) => {
const [tab, setTab] = useState('login'); // 'login' or 'register'
const [email, setEmail] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const styles = {
container: {
backgroundColor: 'white',
borderRadius: '1rem',
padding: '2rem',
maxWidth: '400px',
margin: '0 auto',
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)',
fontFamily: 'Inter, system-ui, sans-serif',
},
title: {
fontSize: '1.3rem',
fontWeight: 'bold',
color: '#1e293b',
marginBottom: '1.5rem',
textAlign: 'center',
},
tabContainer: {
display: 'flex',
justifyContent: 'center',
marginBottom: '1.5rem',
},
tabButton: (active) => ({
cursor: 'pointer',
padding: '0.5rem 1rem',
borderBottom: active ? '2px solid #2563eb' : '2px solid transparent',
fontWeight: active ? 'bold' : 'normal',
color: active ? '#2563eb' : '#64748b',
background: 'none',
border: 'none',
outline: 'none',
fontSize: '1rem',
margin: '0 0.5rem',
}),
input: {
display: 'block',
padding: '0.75rem 1rem',
marginBottom: '1rem',
borderRadius: '0.5rem',
border: '1px solid #cbd5e1',
fontSize: '0.9rem',
outline: 'none',
boxSizing: 'border-box',
width: '100%',
},
inputWrapper: {
width: '100%',
},
button: {
display: 'block',
width: '100%',
padding: '0.75rem 1.5rem',
borderRadius: '0.5rem',
border: 'none',
fontSize: '0.75rem',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.3s ease',
textTransform: 'uppercase',
backgroundColor: '#2563eb',
color: 'white',
},
};
const handleLogin = async (e) => {
e.preventDefault();
if (!username || !password) {
alert('Username dan password wajib diisi');
return;
}
try {
const res = await fetch('https://bot.kediritechnopark.com/webhook/user-dev/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
const err = await res.text();
alert(`Login gagal: ${err}`);
return;
}
const data = await res.json();
const token = data[0].token;
if (token) {
document.cookie = `token=${token}; path=/; max-age=${7 * 24 * 60 * 60}`;
if (postLoginAction) {
postLoginAction(); // resume action (e.g., checkout)
setPostLoginAction(null);
}
} else {
alert('Token tidak ditemukan pada respons login');
}
} catch (error) {
alert('Terjadi kesalahan saat login');
console.error(error);
}
};
const handleRegister = async (e) => {
e.preventDefault();
if (!email || !username || !password) {
alert('Email, username, dan password wajib diisi');
return;
}
try {
const res = await fetch('https://bot.kediritechnopark.com/webhook/user-dev/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, username, password }),
});
if (!res.ok) {
const err = await res.text();
alert(`Registrasi gagal: ${err}`);
return;
}
alert('Registrasi berhasil! Silakan login.');
setTab('login');
setEmail('');
setUsername('');
setPassword('');
} catch (error) {
alert('Terjadi kesalahan saat registrasi');
console.error(error);
}
};
return (
<div style={styles.container}>
<h2 style={styles.title}>{tab === 'login' ? 'Login' : 'Register'}</h2>
<div style={styles.tabContainer}>
<button
style={styles.tabButton(tab === 'login')}
onClick={() => setTab('login')}
type="button"
>
Login
</button>
<button
style={styles.tabButton(tab === 'register')}
onClick={() => setTab('register')}
type="button"
>
Register
</button>
</div>
{tab === 'login' ? (
<form onSubmit={handleLogin}>
<div style={styles.inputWrapper}>
<input
style={styles.input}
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="username"
/>
</div>
<div style={styles.inputWrapper}>
<input
style={styles.input}
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
/>
</div>
<button
type="submit"
style={styles.button}
onMouseOver={(e) => (e.target.style.backgroundColor = '#1d4ed8')}
onMouseOut={(e) => (e.target.style.backgroundColor = '#2563eb')}
>
Masuk
</button>
</form>
) : (
<form onSubmit={handleRegister}>
<div style={styles.inputWrapper}>
<input
style={styles.input}
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
/>
</div>
<div style={styles.inputWrapper}>
<input
style={styles.input}
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="username"
/>
</div>
<div style={styles.inputWrapper}>
<input
style={styles.input}
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="new-password"
/>
</div>
<button
type="submit"
style={styles.button}
onMouseOver={(e) => (e.target.style.backgroundColor = '#1d4ed8')}
onMouseOut={(e) => (e.target.style.backgroundColor = '#2563eb')}
>
Daftar
</button>
</form>
)}
</div>
);
};
export default LoginRegister;

View File

@@ -0,0 +1,95 @@
.container {
background-color: white;
border-radius: 1rem;
padding: 2rem;
max-width: 700px;
margin: 0 auto;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
font-family: 'Inter', system-ui, sans-serif;
text-align: center;
}
.image {
width: 100%;
height: 260px;
background-color: #e2e8f0;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
font-size: 1rem;
margin-bottom: 1.5rem;
}
.headerRow {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
text-align: center;
}
.title {
font-size: 1.1rem;
font-weight: bold;
color: #1e293b;
margin: 0;
}
.price {
font-size: 1.1rem;
font-weight: bold;
color: #2563eb; /* default color, bisa override di inline style */
}
.description {
font-size: 1rem;
color: #64748b;
margin-bottom: 1.5rem;
line-height: 1.6;
}
.buttonGroup {
display: flex;
justify-content: space-between;
gap: 1rem;
}
.button {
padding: 0.75rem 0.4rem;
border-radius: 0.5rem;
border: none;
font-size: 0.55rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
}
.addToCartButton {
background-color: #fbbf24;
color: #1e293b;
margin-right: 0.5rem;
flex: 1;
}
.checkoutButton {
background-color: #2563eb;
color: white;
flex: 1;
}
/* Responsive untuk mobile */
@media (max-width: 600px) {
.container {
text-align: left;
}
.headerRow {
justify-content: flex-start;
}
.buttonGroup {
gap: 0.5rem;
}
}

View File

@@ -0,0 +1,133 @@
import React, { useEffect, useState } from 'react';
import styles from './ProductDetail.module.css';
const ProductDetail = ({ product, setPostLoginAction, setShowedModal }) => {
const [inCart, setInCart] = useState(false);
useEffect(() => {
const existingCookie = document.cookie
.split('; ')
.find(row => row.startsWith('itemsId='));
let items = [];
if (existingCookie) {
try {
const value = decodeURIComponent(existingCookie.split('=')[1]);
items = JSON.parse(value);
if (!Array.isArray(items)) items = [];
} catch (e) {
items = [];
}
}
setInCart(items.includes(product.id));
}, [product.id]);
const onSetCart = () => {
const existingCookie = document.cookie
.split('; ')
.find(row => row.startsWith('itemsId='));
let items = [];
if (existingCookie) {
try {
const value = decodeURIComponent(existingCookie.split('=')[1]);
items = JSON.parse(value);
if (!Array.isArray(items)) items = [];
} catch (e) {
items = [];
}
}
let updatedItems;
if (items.includes(product.id)) {
updatedItems = items.filter(id => id !== product.id); // remove
setInCart(false);
} else {
updatedItems = [...items, product.id]; // add
setInCart(true);
}
document.cookie = `itemsId=${JSON.stringify(updatedItems)}; path=/; max-age=${7 * 24 * 60 * 60
}`;
};
const onCheckout = () => {
// Ambil token dari cookie
const tokenCookie = document.cookie
.split('; ')
.find(row => row.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
// Ambil itemsId dari cookie
const itemsCookie = document.cookie
.split('; ')
.find(row => row.startsWith('itemsId='));
let items = [];
if (itemsCookie) {
try {
items = JSON.parse(itemsCookie.split('=')[1]);
if (!Array.isArray(items)) items = [];
} catch (e) {
items = [];
}
}
if (!items.includes(product.id)) {
items.push(product.id);
}
// Encode items ke string untuk query param
const itemsParam = JSON.stringify(items);
if (!tokenCookie) {
setPostLoginAction(() => () => onCheckout()); // remember intent
setShowedModal('login');
return;
}
// Redirect dengan token dan itemsId di query
window.location.href = `http://localhost:3001/?token=${token}&itemsId=${itemsParam}&redirect_uri=http://localhost:3000/courses&redirect_failed=http://localhost:3000`;
};
// Override harga warna jika free
const priceColor = product.price === 0 ? '#059669' : '#2563eb';
return (
<div className={styles.container}>
<div className={styles.image}>📦</div>
<div className={styles.headerRow}>
<h2 className={styles.title}>{product.name}</h2>
<div className={styles.price} style={{ color: priceColor }}>
{product.price === 0
? 'Free'
: `Rp ${parseInt(product.price).toLocaleString('id-ID')}`}
</div>
</div>
<p className={styles.description}>{product.description}</p>
<div className={styles.buttonGroup}>
<button
className={`${styles.button} ${styles.addToCartButton}`}
onClick={onSetCart}
onMouseOver={e => (e.target.style.backgroundColor = '#facc15')}
onMouseOut={e => (e.target.style.backgroundColor = '#fbbf24')}
>
{inCart ? 'Hapus dari Keranjang' : '+ Keranjang'}
</button>
<button
className={`${styles.button} ${styles.checkoutButton}`}
onClick={onCheckout}
onMouseOver={e => (e.target.style.backgroundColor = '#1d4ed8')}
onMouseOut={e => (e.target.style.backgroundColor = '#2563eb')}
>
Checkout
</button>
</div>
</div>
);
};
export default ProductDetail;

View File

@@ -1,7 +1,9 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Container, Row, Col, Card } from 'react-bootstrap'; import { Container } from 'react-bootstrap';
import styles from './Styles.module.css';
const ProductSection = () => {
const ProductSection = ({ hoveredCard, setHoveredCard, setSelectedProduct, setShowedModal, productSectionRef }) => {
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
useEffect(() => { useEffect(() => {
@@ -18,27 +20,53 @@ const ProductSection = () => {
}, []); }, []);
return ( return (
<section id="produk" className="product-section py-5 bg-light">
<section id="services" className="services py-5">
<Container> <Container>
<div className="section-heading text-center mb-4"> <div className="section-heading text-center mb-4">
<h4>OUR <em>PRODUCT</em></h4> <h4>OUR <em>PRODUCTS</em></h4>
<img src="/assets/images/heading-line-dec.png" alt="" /> <img src="/assets/images/heading-line-dec.png" alt="" />
<p>Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.</p> <p>Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.</p>
</div> </div>
<Row className="justify-content-center"> <div className={styles.coursesGrid}>
<Col lg={12}> {products &&
<div className="d-flex overflow-auto"> products[0]?.name &&
{products.map((product, idx) => ( products.map(product => (
<Card key={idx} className="text-center me-3" style={{ minWidth: '200px' }}> <div
<Card.Img variant="top" src={product.image || '/assets/images/placeholder.png'} alt={product.name} /> key={product.id}
<Card.Body> className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
<Card.Title>{product.name}</Card.Title> onClick={() => {
</Card.Body> setSelectedProduct(product);
</Card> setShowedModal('product');
}}
onMouseEnter={() => setHoveredCard(product.id)}
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.price == null
? 'Pay-As-You-Go'
: `Rp ${product.price.toLocaleString('id-ID')}`}
</span>
</div>
</div>
</div>
))} ))}
</div> </div>
</Col>
</Row>
</Container> </Container>
</section> </section>
); );

View File

@@ -0,0 +1,550 @@
/* TechnoAcademyWebsite.module.css */
/* Header */
.modal {
position: fixed;
width: 100%;
height: 100%;
top: 0;
color: white;
background-color: rgba(0, 0, 0, 0.527);
text-align: center;
display: flex;
justify-content: center;
align-items: center;
z-index: 1001;
}
.modalBody {
color: white;
text-align: center;
display: flex;
align-items: center;
}
.header {
background-color: white;
padding: 14px 6rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 1000;
}
.logo {
display: flex;
align-items: center;
font-size: 1.2rem;
font-weight: bold;
width: 130px;
}
.nav {
display: flex;
gap: 2rem;
align-items: center;
}
.navLink {
text-decoration: none;
color: #64748b;
font-weight: 500;
font-size: 0.95rem;
transition: color 0.3s ease;
cursor: pointer;
}
.navLinkHover {
color: #2563eb;
}
.authButtons {
display: flex;
gap: 1rem;
align-items: center;
}
.searchButton,
.userButton {
background-color: #2563eb;
color: white;
border: none;
border-radius: 50%;
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.loginButton {
background-color: transparent;
color: #64748b;
border: none;
font-size: 0.9rem;
cursor: pointer;
padding: 0.5rem 1rem;
}
/* Hero Section */
.hero {
background-image: url('https://academy.kediritechnopark.com/wp-content/uploads/2025/07/pexels-fauxels-3184360-scaled.jpg');
background-size: cover;
background-position: center;
padding: 0 2rem;
color: white;
position: relative;
border-radius: 0 0 0 60px;
text-align: left;
height: calc(100vh - 61.61px);
display: flex;
}
.heroContainer {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: center;
}
.heroContent {
z-index: 2;
}
.heroTitle {
font-size: 3.5rem;
font-weight: bold;
margin-top: 0px;
margin-bottom: 1.5rem;
line-height: 1.1;
}
.heroDescription {
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 2rem;
opacity: 0.9;
}
.joinButton {
background: linear-gradient(135deg, rgb(59, 130, 246) 0%, rgb(30, 64, 175) 100%);
color: white;
border: none;
padding: 1rem 2rem;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
}
.heroImage {
display: flex;
justify-content: center;
align-items: center;
}
.heroImagePlaceholder {
width: 500px;
height: 350px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 1rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: white;
border: 2px dashed rgba(255, 255, 255, 0.3);
}
.Section {
padding: 2rem 6rem;
background-color: #f8fafc;
}
.coursesContainer {
max-width: 1200px;
margin: 0 auto;
}
.coursesTitle {
font-size: 2.5rem;
font-weight: bold;
color: #1e293b;
margin-bottom: 3rem;
}
.coursesGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
}
.courseCard {
background-color: white;
border-radius: 1rem;
overflow: hidden;
box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
cursor: pointer;
/* Tambahan untuk menghilangkan highlight biru */
-webkit-tap-highlight-color: transparent;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline: none;
}
.courseCard:focus {
outline: none;
}
.courseCardHover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.courseImage {
width: 100%;
height: 200px;
background-color: #e2e8f0;
position: relative;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
.courseLabel {
position: absolute;
top: 1rem;
right: 1rem;
background-color: #ef4444;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.8rem;
font-weight: 600;
}
.courseContent {
padding: 1.5rem;
text-align: left;
}
.courseCategory {
font-size: 0.8rem;
color: #64748b;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.courseTitle {
font-size: 1.2rem;
font-weight: bold;
color: #1e293b;
margin-bottom: 1rem;
margin-top: 0;
}
.courseStats {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
font-size: 0.9rem;
color: #64748b;
}
.courseStat {
display: flex;
align-items: center;
gap: 0.25rem;
}
.coursePrice {
display: flex;
align-items: center;
justify-content: space-between;
}
.originalPrice {
text-decoration: line-through;
color: #94a3b8;
font-size: 0.9rem;
}
.currentPrice {
font-size: 1.2rem;
font-weight: bold;
color: #2563eb;
}
.freePrice {
font-size: 1.2rem;
font-weight: bold;
color: #059669;
}
.featuresContainer {
max-width: 1200px;
margin: 0 auto;
}
.featuresTitle {
font-size: 2.5rem;
font-weight: bold;
color: #1e293b;
margin-bottom: 2rem;
margin-top: 0;
text-align: left;
}
.featuresDescription {
font-size: 1.1rem;
color: #64748b;
line-height: 1.6;
margin-bottom: 3rem;
text-align: left;
max-width: 800px;
}
.featuresList {
display: grid;
gap: 2rem;
}
.featureItem {
display: flex;
gap: 1.5rem;
background-color: #f8fafc;
border-radius: 1rem;
align-items: flex-start;
}
.featureIcon {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #2563eb;
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 1.5rem;
}
.featureContent {
flex: 1;
}
.featureTitle {
font-size: 1.3rem;
font-weight: bold;
color: #1e293b;
margin-top: 6px;
margin-bottom: 0.5rem;
text-align: left;
}
.featureDescription {
color: #64748b;
line-height: 1.6;
text-align: left;
}
.ctaContainer {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
}
.ctaCard {
background-color: white;
padding: 3rem;
border-radius: 1rem;
text-align: center;
box-shadow: 10px 10px 10px rgb(0 0 0 / 15%);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.ctaIcon {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #2563eb;
color: white;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 2rem;
font-size: 2rem;
}
.ctaTitle {
font-size: 1.5rem;
font-weight: bold;
color: #1e293b;
margin-bottom: 1rem;
}
.ctaDescription {
color: #64748b;
line-height: 1.6;
margin-bottom: 2rem;
}
.ctaButton {
background-color: #2563eb;
color: white;
border: none;
padding: 1rem 2rem;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
}
/* Footer */
.footer {
background-color: #1e293b;
color: white;
padding: 2rem;
text-align: center;
}
.footerContent {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.footerText {
font-size: 0.9rem;
color: #94a3b8;
}
.socialLinks {
display: flex;
gap: 1rem;
}
.socialLink {
width: 40px;
height: 40px;
background-color: #374151;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
color: white;
text-decoration: none;
transition: all 0.3s ease;
cursor: pointer;
}
.socialLink:hover {
background-color: #4b5563;
}
/* Responsive Styles */
@media (max-width: 800px) {
.modalBody {
width: 80%;
}
.header {
padding: 14px 2rem;
}
.heroContainer {
grid-template-columns: 1fr;
text-align: center;
}
.ctaTitle,
.courseTitle {
font-size: 15px;
margin: 0;
}
.ctaDescription,
.courseContent p {
font-size: 13px;
margin: 6px 0px;
}
.ctaCard,
.Section {
padding: 2rem 0.8rem;
background-color: #f8fafc;
}
.courseContent {
text-align: left;
padding: 0.8rem;
}
.ctaContainer,
.coursesGrid {
grid-template-columns: repeat(auto-fit, minmax(138px, 1fr));
gap: 0.8rem;
}
.featureTitle {
margin-top: 6px;
margin-bottom: 0.5rem;
text-align: left;
}
.featureDescription {
text-align: left;
margin-bottom: 0;
}
.featuresList {
gap: 0rem;
}
.ctaButton {
font-size: 12px;
padding: 1rem 1rem;
margin-top: 10px;
}
}
@media (max-width: 600px) {
.nav {
display: none;
}
}