ok
This commit is contained in:
54
package-lock.json
generated
54
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
BIN
public/kediri-technopark-logo.png
Normal file
BIN
public/kediri-technopark-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
141
src/App.js
141
src/App.js
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
240
src/components/Login.js
Normal 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;
|
||||||
95
src/components/ProductDetail.module.css
Normal file
95
src/components/ProductDetail.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/components/ProductDetailPage.js
Normal file
133
src/components/ProductDetailPage.js
Normal 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;
|
||||||
@@ -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(() => {
|
||||||
@@ -10,7 +12,7 @@ const ProductSection = () => {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ type: 'product', onlyParents:true }),
|
body: JSON.stringify({ type: 'product', onlyParents: true }),
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => setProducts(data))
|
.then(data => setProducts(data))
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
550
src/components/Styles.module.css
Normal file
550
src/components/Styles.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user