ok
This commit is contained in:
12
src/App.js
12
src/App.js
@@ -9,8 +9,11 @@ import ServicesSection from './components/ServicesSection';
|
||||
import ProductSection from './components/ProductSection';
|
||||
import AcademySection from './components/AcademySection';
|
||||
import AboutUsSection from './components/AboutUsSection';
|
||||
import KnowledgeBaseSection from './components/KnowledgeBaseSection';
|
||||
import ClientsSection from './components/ClientsSection';
|
||||
// KnowledgeBaseSection hidden temporarily
|
||||
// import KnowledgeBaseSection from './components/KnowledgeBaseSection';
|
||||
// ClientsSection hidden temporarily
|
||||
// import ClientsSection from './components/ClientsSection';
|
||||
import FAQSection from './components/FAQSection';
|
||||
import Footer from './components/Footer';
|
||||
import ProductDetailPage from './components/ProductDetailPage';
|
||||
import Dashboard from './components/Dashboard';
|
||||
@@ -44,8 +47,9 @@ function HomePage({
|
||||
setShowedModal={setShowedModal}
|
||||
setWillDo={setWillDo}
|
||||
/>
|
||||
<KnowledgeBaseSection />
|
||||
<ClientsSection />
|
||||
{/* <KnowledgeBaseSection /> */}
|
||||
{/* <ClientsSection /> */}
|
||||
<FAQSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,37 @@
|
||||
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 { ref, inView } = useInView();
|
||||
return (
|
||||
<section id="about" className="about-us section pt-5">
|
||||
<Container>
|
||||
<Row className="align-items-center">
|
||||
<Col lg={6}>
|
||||
<div className="section-heading">
|
||||
<span style={{ color: '#6a59ff', fontWeight: 'bold' }}>Kediri Technopark</span>
|
||||
<h2 className="mt-2">ABOUT US</h2>
|
||||
<img src="/assets/images/heading-line-dec.png" alt="" />
|
||||
<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>
|
||||
<div className="mt-4 d-flex gap-3">
|
||||
<Button href="https://instagram.com/kediri.technopark" className="px-4 py-2 rounded-pill text-white" style={{ background: 'linear-gradient(to right, #6a59ff, #8261ee)', border: 'none' }}>
|
||||
Instagram
|
||||
</Button>
|
||||
<Button href="https://wa.me/6281318894994" target="_blank" variant="outline-success" className="px-4 py-2 rounded-pill">
|
||||
<i className="fab fa-whatsapp"></i> WhatsApp
|
||||
</Button>
|
||||
<section id="about" ref={ref} className={`${styles.aboutSection} ${shared.revealSection} ${inView ? shared.isVisible : ''}`}>
|
||||
<AnimatedBackground /> {/* Komponen animasi sebagai latar belakang */}
|
||||
<div className={styles.contentWrapper}>
|
||||
<Container>
|
||||
<Row className="justify-content-center">
|
||||
<Col lg={8}>
|
||||
<div className={styles.textContent}>
|
||||
<h2 className={styles.sectionTitle}>Tentang Kami</h2>
|
||||
<p className={styles.paragraph}>
|
||||
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>
|
||||
<ul className={styles.valueList}>
|
||||
<li><CheckCircle size={20} className={styles.listIcon} /><span>Inovasi Berkelanjutan</span></li>
|
||||
<li><CheckCircle size={20} className={styles.listIcon} /><span>Kolaborasi Komunitas</span></li>
|
||||
<li><CheckCircle size={20} className={styles.listIcon} /><span>Pemberdayaan Talenta</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<div className="right-image">
|
||||
<img src="/assets/images/about-right-dec.png" alt="" className="img-fluid" />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutUsSection;
|
||||
export default AboutUsSection;
|
||||
|
||||
61
src/components/AboutUsSection.module.css
Normal file
61
src/components/AboutUsSection.module.css
Normal file
@@ -0,0 +1,61 @@
|
||||
.aboutSection {
|
||||
padding: 80px 0; /* Ditinggikan dari 60px */
|
||||
position: relative;
|
||||
background-color: #0f172a; /* Latar belakang biru gelap (slate-900) */
|
||||
overflow: hidden; /* Mencegah canvas keluar dari section */
|
||||
}
|
||||
|
||||
/* 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,32 +1,60 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Container } from 'react-bootstrap';
|
||||
import styles from './Styles.module.css';
|
||||
import useInView from '../hooks/useInView';
|
||||
|
||||
const AcademySection = ({setSelectedProduct, setShowedModal, courseSectionRef, setWillDo}) => {
|
||||
const [products, setProducts] = useState([]);
|
||||
const [hoveredCard, setHoveredCard] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch all items to compute module/sessions reliably, then filter courses client-side
|
||||
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ type: 'course' }),
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
.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));
|
||||
}, []);
|
||||
|
||||
const { ref, inView } = useInView();
|
||||
return (
|
||||
|
||||
<section id="services" className="services pt-5" ref={courseSectionRef}>
|
||||
<section id="academy" 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>
|
||||
<div className="section-heading mb-4">
|
||||
<h4>OUR <em>ACADEMY PROGRAM</em></h4>
|
||||
<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>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className={styles.sectionEyebrow}>Academy</div>
|
||||
<h2 className={styles.sectionTitle}>Our Academy Program</h2>
|
||||
<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 className={styles.coursesGrid}>
|
||||
@@ -51,7 +79,14 @@ const AcademySection = ({setSelectedProduct, setShowedModal, courseSectionRef, s
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.courseContentTop}>
|
||||
<h3 className={styles.courseTitle}>{product.name}</h3>
|
||||
<div className={styles.titleRow}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,12 +104,12 @@ const AcademySection = ({setSelectedProduct, setShowedModal, courseSectionRef, s
|
||||
: `Rp ${product.price.toLocaleString('id-ID')}`}
|
||||
</span>
|
||||
</div>
|
||||
<button className="px-4 py-2 rounded-pill text-white" style={{ background: 'linear-gradient(to right, #6a59ff, #8261ee)', border: 'none' }}
|
||||
<button className={`${styles.enrollButton}`}
|
||||
onClick={() => {
|
||||
setSelectedProduct(product);
|
||||
setShowedModal('product');
|
||||
setWillDo('checkout');
|
||||
}}>Beli</button>
|
||||
}}>Daftar</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
120
src/components/AnimatedBackground.js
Normal file
120
src/components/AnimatedBackground.js
Normal file
@@ -0,0 +1,120 @@
|
||||
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 = [];
|
||||
const particleCount = 70;
|
||||
|
||||
function Particle(x, y, vx, vy) {
|
||||
this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.radius = 1.5;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
const connectionDistance = 90;
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
p.y += p.vy;
|
||||
|
||||
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();
|
||||
window.addEventListener('resize', setupCanvas);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
window.removeEventListener('resize', setupCanvas);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <canvas ref={canvasRef} className={styles.particleCanvas}></canvas>;
|
||||
};
|
||||
|
||||
export default AnimatedBackground;
|
||||
7
src/components/AnimatedBackground.module.css
Normal file
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,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Container, Row, Col, Image } from 'react-bootstrap';
|
||||
import styles from './Styles.module.css';
|
||||
import useInView from '../hooks/useInView';
|
||||
|
||||
const ClientsSection = () => {
|
||||
const logos = [
|
||||
@@ -10,8 +11,9 @@ const ClientsSection = () => {
|
||||
'psi.png',
|
||||
];
|
||||
|
||||
const { ref, inView } = useInView();
|
||||
return (
|
||||
<section id="clients" className="the-clients section py-5">
|
||||
<section id="clients" ref={ref} className={`the-clients section py-5 ${styles.revealSection} ${inView ? styles.isVisible : ''}`}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
207
src/components/CoverflowCarousel.js
Normal file
207
src/components/CoverflowCarousel.js
Normal file
@@ -0,0 +1,207 @@
|
||||
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);
|
||||
};
|
||||
|
||||
// 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={() => { 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) => { onCardClick && onCardClick(p); }}
|
||||
isCenter={position === 0}
|
||||
canHover={position === 0 && animationState === 'ready' && !shiftDirection && !isDragging}
|
||||
/>
|
||||
</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;
|
||||
575
src/components/CoverflowCarousel.module.css
Normal file
575
src/components/CoverflowCarousel.module.css
Normal file
@@ -0,0 +1,575 @@
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
margin: 40px 0 60px 0;
|
||||
perspective: 1000px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.container {
|
||||
height: 350px;
|
||||
margin: 20px 0 40px 0;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 220px;
|
||||
height: 270px;
|
||||
}
|
||||
|
||||
.cardWrapper {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.container {
|
||||
height: 320px;
|
||||
margin: 15px 0 35px 0;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 200px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.navButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.navButton svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.dotsContainer {
|
||||
bottom: -18px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
}
|
||||
160
src/components/FAQSection.js
Normal file
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" 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
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: 0 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,32 +1,84 @@
|
||||
import React from 'react';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import styles from './Styles.module.css';
|
||||
import styles from './Footer.module.css';
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer id="contact" className={`bg-dark text-white py-4 ${styles.footer}`}>
|
||||
<footer className={styles.footer}>
|
||||
<Container>
|
||||
<Row className="justify-content-center text-start">
|
||||
<Col lg={6} className="mb-3">
|
||||
<h4>Contact Us</h4>
|
||||
<p>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 noreferrer">@kediri.technopark</a></p>
|
||||
<p><a href="https://kediritechnopark.com" target="_blank" rel="noopener noreferrer">www.KEDIRITECHNOPARK.com</a></p>
|
||||
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<div className="footer-widget">
|
||||
<h4>About Our Company</h4>
|
||||
<div className={styles.logo}>
|
||||
<img src="https://kediritechnopark.com/kediri-technopark-logo-white.png" alt="Logo" />
|
||||
</div>
|
||||
<p>Kediri Technopark adalah pusat pengembangan inovasi digital dan aplikasi untuk masyarakat dan pelaku usaha.</p>
|
||||
<Row className={styles.footerContent}>
|
||||
<Col lg={3} md={6} className={styles.footerColumn}>
|
||||
<div className={styles.footerLogo}>
|
||||
<img
|
||||
src="https://kediritechnopark.com/kediri-technopark-logo-white.png"
|
||||
alt="Kediri Technopark Logo"
|
||||
className={styles.logoImage}
|
||||
/>
|
||||
</div>
|
||||
<p className={styles.companyDescription}>
|
||||
Kediri Technopark adalah pusat pengembangan inovasi digital dan aplikasi untuk masyarakat dan pelaku usaha.
|
||||
</p>
|
||||
<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>
|
||||
</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>
|
||||
</Col>
|
||||
<Col lg={12} className="mt-3">
|
||||
<div className="copyright-text">
|
||||
|
||||
<Col lg={3} md={6} className={styles.footerColumn}>
|
||||
<h3 className={styles.footerTitle}>Contact Us</h3>
|
||||
<div className={styles.contactInfo}>
|
||||
<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>
|
||||
</Col>
|
||||
|
||||
<Col lg={3} md={6} className={styles.footerColumn}>
|
||||
<h3 className={styles.footerTitle}>Quick Links</h3>
|
||||
<ul className={styles.footerLinks}>
|
||||
<li><a href="#home">Home</a></li>
|
||||
<li><a href="#products">Products</a></li>
|
||||
<li><a href="#academy">Academy</a></li>
|
||||
<li><a href="#about">About Us</a></li>
|
||||
<li><a href="#contact">Contact</a></li>
|
||||
</ul>
|
||||
</Col>
|
||||
|
||||
<Col lg={3} md={6} className={styles.footerColumn}>
|
||||
<h3 className={styles.footerTitle}>Newsletter</h3>
|
||||
<div className={styles.newsletter}>
|
||||
<p>Subscribe to our newsletter for the latest updates</p>
|
||||
<div className={styles.newsletterForm}>
|
||||
<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>
|
||||
</Col>
|
||||
|
||||
252
src/components/Footer.module.css
Normal file
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;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.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 styles from './Styles.module.css';
|
||||
|
||||
@@ -6,10 +6,30 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
||||
const navigate = useNavigate();
|
||||
const [hoveredNav, setHoveredNav] = useState(null);
|
||||
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', block: 'start' });
|
||||
// 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);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<img src="./kediri-technopark-logo.png" className={styles.logo} alt="Logo" />
|
||||
<header className={`${styles.header} ${isScrolled ? styles.headerScrolled : ''}`}>
|
||||
<img src="./kediri-technopark-logo.png" className={styles.logo} alt="Logo" />
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className={styles.nav}>
|
||||
@@ -19,7 +39,23 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
||||
onMouseLeave={() => setHoveredNav(null)}
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
HOME
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
className={`${styles.navLink} ${hoveredNav === 21 ? styles.navLinkHover : ''}`}
|
||||
onMouseEnter={() => setHoveredNav(21)}
|
||||
onMouseLeave={() => setHoveredNav(null)}
|
||||
onClick={() => scrollToId('about')}
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
className={`${styles.navLink} ${hoveredNav === 22 ? styles.navLinkHover : ''}`}
|
||||
onMouseEnter={() => setHoveredNav(22)}
|
||||
onMouseLeave={() => setHoveredNav(null)}
|
||||
onClick={() => scrollToId('services')}
|
||||
>
|
||||
Services
|
||||
</a>
|
||||
{username &&
|
||||
<a
|
||||
@@ -29,7 +65,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
||||
onClick={() => {
|
||||
navigate('/dashboard');
|
||||
}}>
|
||||
DASHBOARD
|
||||
Dashboard
|
||||
</a>
|
||||
}
|
||||
{!username &&
|
||||
@@ -38,21 +74,25 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
||||
className={`${styles.navLink} ${hoveredNav === 3 ? styles.navLinkHover : ''}`}
|
||||
onMouseEnter={() => setHoveredNav(3)}
|
||||
onMouseLeave={() => setHoveredNav(null)}
|
||||
onClick={() => {
|
||||
navigate('/products');
|
||||
}}
|
||||
onClick={() => scrollToId('products')}
|
||||
>
|
||||
PRODUCTS
|
||||
Products
|
||||
</a>
|
||||
<a
|
||||
className={`${styles.navLink} ${hoveredNav === 4 ? styles.navLinkHover : ''}`}
|
||||
onMouseEnter={() => setHoveredNav(4)}
|
||||
onMouseLeave={() => setHoveredNav(null)}
|
||||
onClick={() => {
|
||||
scrollToCourse();
|
||||
}}
|
||||
onClick={() => scrollToId('academy')}
|
||||
>
|
||||
ACADEMY
|
||||
Academy
|
||||
</a>
|
||||
<a
|
||||
className={`${styles.navLink} ${hoveredNav === 5 ? styles.navLinkHover : ''}`}
|
||||
onMouseEnter={() => setHoveredNav(5)}
|
||||
onMouseLeave={() => setHoveredNav(null)}
|
||||
onClick={() => scrollToId('faq')}
|
||||
>
|
||||
FAQ
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
@@ -69,11 +109,16 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
||||
{username ? (
|
||||
<>
|
||||
<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={() => {
|
||||
navigate('/dashboard');
|
||||
}}>
|
||||
DASHBOARD
|
||||
Dashboard
|
||||
</button>
|
||||
|
||||
<button className={styles.logoutButton} onClick={() => {
|
||||
@@ -84,15 +129,23 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className={styles.loginButton}
|
||||
onClick={() => {
|
||||
setMenuOpen(false);
|
||||
setShowedModal('login');
|
||||
}}
|
||||
>
|
||||
LOGIN
|
||||
</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
|
||||
className={styles.loginButton}
|
||||
onClick={() => {
|
||||
setMenuOpen(false);
|
||||
setShowedModal('login');
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -107,11 +160,11 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
|
||||
)}
|
||||
{!username && (
|
||||
<button className={styles.loginButton} onClick={() => setShowedModal('login')}>
|
||||
LOGIN
|
||||
Sign in
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
// HeroSection.jsx — 2025 refresh using React-Bootstrap + CSS Module
|
||||
import React from 'react';
|
||||
import { Container, Row, Col, Button } from 'react-bootstrap';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styles from './HeroSection.module.css';
|
||||
|
||||
const HeroSection = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const goProducts = () => navigate('/products');
|
||||
const goAcademy = () => navigate('/#services');
|
||||
|
||||
return (
|
||||
<section className={`${styles.hero} pt-5`}
|
||||
<section className={`${styles.hero} pt-3 pb-3`}
|
||||
aria-label="Kediri Technopark hero section">
|
||||
<Container>
|
||||
<Row className="align-items-center gy-5">
|
||||
{/* Image on top for mobile, text first on lg+ */}
|
||||
<Col xs={{ order: 1 }} lg={{ span: 6, order: 1 }}>
|
||||
<Container className={styles.heroContainer}>
|
||||
<Row className="align-items-center gy-3">
|
||||
{/* Text first for mobile and desktop for clarity */}
|
||||
<Col 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
|
||||
@@ -18,18 +24,27 @@ const HeroSection = () => {
|
||||
<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={goProducts}>
|
||||
Explore Products
|
||||
</Button>
|
||||
<Button variant="light" className={styles.ctaSecondary} onClick={goAcademy}>
|
||||
View Academy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={{ order: 0 }} lg={{ span: 6, order: 2 }}>
|
||||
<Col xs={{ order: 1 }} lg={{ span: 4, order: 2 }} xl={{ span: 5, order: 2 }}>
|
||||
<div className={styles.imageWrap}>
|
||||
<img
|
||||
src="https://kediritechnopark.com/assets/hero.png"
|
||||
alt="Ekosistem digital Kediri Technopark"
|
||||
className={`img-fluid ${styles.heroImage}`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className={styles.glow} aria-hidden="true" />
|
||||
<div className={styles.imageFrame}>
|
||||
<img
|
||||
src="https://kediritechnopark.com/assets/hero.png"
|
||||
alt="Ekosistem digital Kediri Technopark"
|
||||
className={`img-fluid ${styles.heroImage}`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -1,58 +1,141 @@
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
background: radial-gradient(1200px 600px at 10% -10%, rgba(37, 99, 235, 0.15), transparent 60%),
|
||||
radial-gradient(1000px 500px at 110% 10%, rgba(34, 197, 94, 0.15), transparent 60%),
|
||||
var(--surface);
|
||||
overflow: clip;
|
||||
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: var(--hero-maxw);
|
||||
max-width: 600px;
|
||||
overflow: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.kickerRow { margin-bottom: .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.08;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
/* Fluid type: min 28px → max 56px */
|
||||
font-size: clamp(1.75rem, 2.5vw + 1rem, 3.5rem);
|
||||
background: linear-gradient(92deg, var(--text), #3b82f6 40%, #22c55e 90%);
|
||||
/* 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: black;
|
||||
color: transparent;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin-top: 1rem;
|
||||
color: var(--muted);
|
||||
font-size: clamp(1rem, 0.6vw + 0.95rem, 1.2rem);
|
||||
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: 1.25rem;
|
||||
margin-top: 0.75rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.cta {
|
||||
--ring: 0 0 0 0 rgba(37,99,235,0);
|
||||
border-radius: var(--radius-2xl) !important;
|
||||
padding: 0.625rem 1rem !important;
|
||||
.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;
|
||||
backdrop-filter: saturate(140%);
|
||||
transition: transform .2s ease, box-shadow .2s ease, background-color .2s ease;
|
||||
box-shadow: var(--shadow-soft);
|
||||
letter-spacing: 0.02em;
|
||||
transition: background-color .16s ease, border-color .16s ease;
|
||||
margin-right: .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 .16s ease, border-color .16s ease, background-color .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 {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 32px rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
|
||||
.ctaPrimary:focus-visible,
|
||||
.ctaSecondary:hover,
|
||||
.ctaSecondary:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 32px rgba(34, 197, 94, 0.25);
|
||||
box-shadow: var(--shadow-neutral-s);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.imageWrap {
|
||||
@@ -60,33 +143,32 @@
|
||||
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: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 80px; /* lebar gradasi di sisi */
|
||||
pointer-events: none; /* biar nggak ganggu klik */
|
||||
z-index: 2;
|
||||
}
|
||||
.imageWrap::before, .imageWrap::after { content: none; }
|
||||
|
||||
.imageWrap::before {
|
||||
left: 0;
|
||||
background: linear-gradient(to right, rgba(255,255,255,1), rgba(255,255,255,0));
|
||||
.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;
|
||||
}
|
||||
|
||||
.imageWrap::after {
|
||||
right: 0;
|
||||
background: linear-gradient(to left, rgba(255,255,255,1), rgba(255,255,255,0));
|
||||
}
|
||||
|
||||
|
||||
.heroImage {
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-soft);
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 4 / 3;
|
||||
object-fit: cover;
|
||||
border-radius: calc(var(--radius-2xl) - 4px);
|
||||
}
|
||||
|
||||
.glow {
|
||||
@@ -95,15 +177,43 @@
|
||||
height: 40%;
|
||||
filter: blur(40px);
|
||||
z-index: -1;
|
||||
background: radial-gradient(60% 60% at 50% 0%, rgba(59,130,246,.35), transparent 60%),
|
||||
radial-gradient(50% 50% at 30% 60%, rgba(34,197,94,.25), transparent 60%);
|
||||
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; /* slate-500 */
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.statItem strong { color: #0f172a; font-weight: 700; margin-right: 4px; }
|
||||
.statDot { width: 4px; height: 4px; border-radius: 2px; background: #cbd5e1; }
|
||||
|
||||
/* Fine-tuned responsive spacing */
|
||||
@media (min-width: 992px) {
|
||||
.copyWrap { padding-right: 1rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.hero { padding-top: 2rem; }
|
||||
.hero { padding-top: 1.25rem; }
|
||||
.ctaGroup { display: grid; gap: 8px; }
|
||||
.ctaPrimary, .ctaSecondary { width: 100% !important; text-align: center; }
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.imageWrap::before,
|
||||
.imageWrap::after { display: none; }
|
||||
.title { font-size: clamp(1.4rem, 1.8vw + 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; }
|
||||
}
|
||||
|
||||
.imageFrame:hover { box-shadow: none; transform: none; }
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.imageWrap { max-width: 720px; }
|
||||
}
|
||||
51
src/components/ProductCard.js
Normal file
51
src/components/ProductCard.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import styles from './ProductCard.module.css';
|
||||
|
||||
const ProductCard = ({ product, onCardClick, isCenter, canHover }) => {
|
||||
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) => {
|
||||
if (isCenter) {
|
||||
// Clicks on overlay open detail; prevent parent selection
|
||||
e.stopPropagation();
|
||||
onCardClick && onCardClick(product);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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); }}
|
||||
>
|
||||
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;
|
||||
138
src/components/ProductCard.module.css
Normal file
138
src/components/ProductCard.module.css
Normal file
@@ -0,0 +1,138 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.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: 576px) {
|
||||
.title { font-size: 0.9rem; }
|
||||
.overlay { --overlay-collapsed: 56px; }
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 [products, setProducts] = useState([]);
|
||||
const [hoveredCard, setHoveredCard] = useState(null);
|
||||
// Define this function outside useEffect so it can be called anywhere
|
||||
const [filteredProducts, setFilteredProducts] = useState([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
|
||||
|
||||
// Inside your component
|
||||
useEffect(() => {
|
||||
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
|
||||
method: 'POST',
|
||||
@@ -21,70 +21,79 @@ const ProductSection = ({ setSelectedProduct, setShowedModal, productSectionRef,
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const enrichedData = processProducts(data);
|
||||
setProducts(enrichedData);
|
||||
const processed = processProducts(data);
|
||||
setProducts(processed);
|
||||
setFilteredProducts(processed);
|
||||
})
|
||||
.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" ref={productSectionRef}>
|
||||
// Filter products by category
|
||||
useEffect(() => {
|
||||
if (selectedCategory === 'all') {
|
||||
setFilteredProducts(products);
|
||||
} else {
|
||||
setFilteredProducts(products.filter(product => product.category === selectedCategory));
|
||||
}
|
||||
}, [selectedCategory, products]);
|
||||
|
||||
// Handle product selection for detail view
|
||||
const handleViewDetail = (product) => {
|
||||
setSelectedProduct(product);
|
||||
setShowedModal('product');
|
||||
setWillDo('checkout');
|
||||
};
|
||||
|
||||
const { ref, inView } = useInView();
|
||||
return (
|
||||
<section id="products" className={`${styles.productSection} ${shared.revealSection} ${inView ? shared.isVisible : ''}`} ref={(el) => {
|
||||
if (typeof productSectionRef === 'function') productSectionRef(el);
|
||||
if (ref) ref.current = el;
|
||||
}}>
|
||||
<Container>
|
||||
<div className="section-heading mb-4">
|
||||
<h4>OUR <em>PRODUCTS</em></h4>
|
||||
<img src="/assets/images/heading-line-dec.png" alt="" />
|
||||
<p>Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.</p>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2 className={styles.sectionTitle}>Produk Unggulan</h2>
|
||||
<p>Produk digital siap pakai untuk mempercepat pertumbuhan bisnis Anda.</p>
|
||||
</div>
|
||||
<div className={styles.coursesGrid}>
|
||||
{products &&
|
||||
products[0]?.name &&
|
||||
products
|
||||
.map(product => (
|
||||
<div
|
||||
key={product.id}
|
||||
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`}
|
||||
onClick={() => {
|
||||
setSelectedProduct(product);
|
||||
setShowedModal('product');
|
||||
}}
|
||||
onMouseEnter={() => setHoveredCard(product.id)}
|
||||
onMouseLeave={() => setHoveredCard(null)}
|
||||
|
||||
{/* Category Filter */}
|
||||
{categories.length > 2 && (
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterWrapper}>
|
||||
<button
|
||||
className={`${styles.filterBtn} ${selectedCategory === 'all' ? styles.active : ''}`}
|
||||
onClick={() => setSelectedCategory('all')}
|
||||
>
|
||||
Semua Produk
|
||||
</button>
|
||||
{categories.filter(cat => cat !== 'all').map(category => (
|
||||
<button
|
||||
key={category}
|
||||
className={`${styles.filterBtn} ${selectedCategory === category ? styles.active : ''}`}
|
||||
onClick={() => setSelectedCategory(category)}
|
||||
>
|
||||
<div>
|
||||
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}>
|
||||
{product.price === 0 && (
|
||||
<span className={styles.courseLabel}>Free</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.courseContentTop}>
|
||||
<h3 className={styles.courseTitle}>{product.name}</h3>
|
||||
<p className={styles.courseDesc}>{product.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.courseContentBottom}>
|
||||
<div className={styles.coursePrice}>
|
||||
<span
|
||||
className={
|
||||
product.price === 0
|
||||
? styles.freePrice
|
||||
: styles.currentPrice
|
||||
}
|
||||
>
|
||||
{product.price == null
|
||||
? 'Pay-As-You-Go'
|
||||
: `Rp ${product.price.toLocaleString('id-ID')}`}
|
||||
</span>
|
||||
</div>
|
||||
<button className="px-4 py-2 rounded-pill text-white" style={{ fontSize: '0.8rem', background: 'linear-gradient(to right, #6a59ff, #8261ee)', border: 'none' }}
|
||||
onClick={() => {
|
||||
setSelectedProduct(product);
|
||||
setShowedModal('product');
|
||||
setWillDo('checkout');
|
||||
}}>Beli</button>
|
||||
</div>
|
||||
</div>
|
||||
{category}
|
||||
</button>
|
||||
))}
|
||||
</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>
|
||||
</section>
|
||||
|
||||
246
src/components/ProductSection.module.css
Normal file
246
src/components/ProductSection.module.css
Normal file
@@ -0,0 +1,246 @@
|
||||
/* 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;
|
||||
padding: 0 56px;
|
||||
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 {
|
||||
padding: 0 60px;
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.noProducts {
|
||||
height: 280px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.productSection {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.filterWrapper {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filterBtn {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.carouselContainer {
|
||||
padding: 0 50px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.noProducts {
|
||||
height: 260px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.productSection {
|
||||
padding: 35px 0;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.sectionHeader p {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.filterContainer {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filterWrapper {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filterBtn {
|
||||
padding: 5px 10px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.carouselContainer {
|
||||
padding: 0 40px;
|
||||
min-height: 370px;
|
||||
}
|
||||
|
||||
.noProducts {
|
||||
height: 240px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.productSection {
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.sectionHeader p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.carouselContainer {
|
||||
padding: 0 35px;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.noProducts {
|
||||
height: 220px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.carouselContainer {
|
||||
padding: 0 30px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.noProducts {
|
||||
height: 200px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,71 @@
|
||||
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 { ref, inView } = useInView();
|
||||
return (
|
||||
<section id="services" className="services py-5">
|
||||
<Container>
|
||||
<div className="section-heading mb-4">
|
||||
<h4>OUR <em>SERVICES</em></h4>
|
||||
<img src="/assets/images/heading-line-dec.png" alt="" />
|
||||
<p>Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.</p>
|
||||
</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>
|
||||
</Row>
|
||||
</Container>
|
||||
<section id="services" ref={ref} className={`${styles.blueprintContainer} ${shared.revealSection} ${inView ? shared.isVisible : ''}`}>
|
||||
<div className={styles.blueprintGrid}></div>
|
||||
<div className={styles.contentWrapper}>
|
||||
<Container>
|
||||
<div className={styles.sectionHeader}>
|
||||
{/* Judul diubah */}
|
||||
<h2 className={styles.sectionTitle}>Layanan Kami</h2>
|
||||
<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>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServicesSection;
|
||||
export default ServicesSection;
|
||||
|
||||
183
src/components/ServicesSection.module.css
Normal file
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 {
|
||||
background-color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
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;
|
||||
background: transparent; /* blend with top page */
|
||||
-webkit-backdrop-filter: saturate(140%) blur(14px);
|
||||
backdrop-filter: saturate(140%) blur(14px);
|
||||
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 {
|
||||
@@ -83,12 +96,19 @@
|
||||
}
|
||||
|
||||
.loginButton {
|
||||
background-color: transparent;
|
||||
color: #64748b;
|
||||
background-color: #2563eb; /* brand color */
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
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 */
|
||||
@@ -175,6 +195,53 @@
|
||||
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 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
@@ -184,15 +251,23 @@
|
||||
|
||||
.coursesGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 2rem;
|
||||
grid-template-columns: 1fr;
|
||||
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 {
|
||||
background-color: white;
|
||||
border-radius: 1rem;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #e5e7eb; /* thin border, no base shadow */
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-user-select: none;
|
||||
@@ -210,13 +285,14 @@
|
||||
|
||||
|
||||
.courseCardHover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-3px);
|
||||
border-color: #d1d5db;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.courseImage {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
height: 160px;
|
||||
background-color: white;
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -225,7 +301,7 @@
|
||||
color: #64748b;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
@@ -242,10 +318,17 @@
|
||||
}
|
||||
|
||||
.courseContentTop {
|
||||
padding: 1rem 1rem 0rem;
|
||||
padding: 0.75rem 0.75rem 0rem;
|
||||
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;
|
||||
@@ -253,11 +336,12 @@
|
||||
|
||||
|
||||
.courseContentBottom {
|
||||
padding: 1rem;
|
||||
padding-top: 0;
|
||||
padding: 0.75rem;
|
||||
padding-top: 0.5rem;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.courseCategory {
|
||||
@@ -269,26 +353,59 @@
|
||||
}
|
||||
|
||||
.courseTitle {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #1e293b;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 0;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0; /* allow ellipsis shrink in flex */
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin: 0; /* in-row with pills */
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.courseStats {
|
||||
.pillRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.courseStat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
.pill {
|
||||
padding: 2px 8px;
|
||||
background-color: #f3f4f6;
|
||||
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 {
|
||||
@@ -304,17 +421,49 @@
|
||||
}
|
||||
|
||||
.currentPrice {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: bold;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.freePrice {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: bold;
|
||||
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 {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
@@ -343,6 +492,28 @@
|
||||
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 {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
|
||||
29
src/components/TestAnimation.js
Normal file
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;
|
||||
14
src/hooks/useInView.js
Normal file
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 {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
|
||||
Reference in New Issue
Block a user