Merge remote-tracking branch 'kediritechnopark.com/main'

This commit is contained in:
everythingonblack
2025-08-18 00:06:04 +07:00
32 changed files with 6320 additions and 319 deletions

70
gemini-context.md Normal file
View File

@@ -0,0 +1,70 @@
# Gemini CLI Interaction Context
This document summarizes the key tasks and file modifications performed during the interaction with the Gemini CLI agent.
## 1. Initial Setup & Context
- Acknowledged project context and folder structure.
## 2. Product Section Redesign (Initial - Clean & Professional Look)
- **Goal:** Redesign "Our Products" section for a clean, professional look, and scalability.
- **Files Modified:**
- `src/components/ProductSection.module.css`: Created new CSS module for the section.
- `src/components/ProductCard.module.css`: Created new CSS module for individual product cards.
- `src/components/ProductCard.js`: Created new `ProductCard` component.
- `src/components/ProductSection.js`: Modified to use `ProductCard` and a new grid layout.
## 3. Product Section Redesign (Iteration 2 - Filters & Enhanced Hover)
- **Goal:** Add filtering capabilities and enhance hover effects on product cards.
- **Files Modified:**
- `src/components/ProductSection.module.css`: Added filter styles.
- `src/components/ProductCard.module.css`: Added category tags and enhanced hover effects (image zoom, button slide-up).
- `src/components/ProductCard.js`: Included category tags.
- `src/components/ProductSection.js`: Added filtering logic and filter buttons.
## 4. Copywriting Redesign (AIDA Framework)
- **Goal:** Apply AIDA framework to the section's main heading and description.
- **Files Modified:**
- `src/components/ProductSection.js`: Updated main heading and description.
## 5. Remove "Pelatihan" Category
- **Goal:** Remove "Pelatihan" (Training) category from the product section.
- **Files Modified:**
- `src/components/ProductSection.js`: Removed "Pelatihan" from categories/filters and updated copywriting.
## 6. Product Card Redesign (Overlay on Hover)
- **Goal:** Implement a full-card image with an overlay on hover for product details and CTAs.
- **Files Modified:**
- `src/components/ProductCard.module.css`: Rewrote for overlay-on-hover design.
- `src/components/ProductCard.js`: Implemented new overlay structure.
## 7. Carousel Implementation (Custom)
- **Goal:** Change product display to a custom carousel with a centered active item.
- **Files Modified:**
- `src/components/ProductSection.module.css`: Added custom carousel styles.
- `src/components/ProductSection.js`: Implemented custom carousel logic.
## 8. Carousel Implementation (React-Slick)
- **Goal:** Refactor to use the existing `react-slick` library for the carousel.
- **Files Modified:**
- `src/components/ProductSection.module.css`: Updated to use `react-slick` specific styles.
- `src/components/ProductSection.js`: Refactored to use `react-slick` component, removing custom carousel logic.
## 9. Carousel Tuning (Size & Centering Iterations)
- **Goal:** Adjust carousel size, centering, and visibility of navigation.
- **Files Modified:**
- `src/components/ProductSection.module.css`: Multiple updates to `slidesToShow`, scaling, arrow positioning, and adding/removing `carouselWrapper`.
- `src/components/ProductSection.js`: Multiple updates to `sliderSettings`.
## 10. Interactive Gallery Design
- **Goal:** Implement a completely new "Interactive Gallery" design (featured product + thumbnail navigation).
- **Files Modified:**
- `src/components/ProductSection.module.css`: Completely rewrote for gallery layout.
- `src/components/ProductSection.js`: Heavily refactored to remove `react-slick` and implement gallery logic. `ProductCard.js` was effectively deprecated in this step.
## 11. Minimalist Masonry Grid (Current Task)
- **Goal:** Implement a "Minimalist Masonry Grid" layout for product display.
- **Files Modified:**
- `src/components/ProductCard.module.css`: Modified to revert to a simple, always-visible card design suitable for masonry.
- `src/components/ProductCard.js`: (Pending user approval for modification)
- `src/components/ProductSection.module.css`: (Pending modification for masonry grid styles)
- `src/components/ProductSection.js`: (Pending modification for masonry grid logic)

2809
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"bootstrap": "^5.3.7", "bootstrap": "^5.3.7",
"framer-motion": "^12.23.12",
"lucide-react": "^0.536.0", "lucide-react": "^0.536.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
@@ -15,6 +16,7 @@
"react-router-dom": "^7.7.1", "react-router-dom": "^7.7.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-slick": "^0.30.3", "react-slick": "^0.30.3",
"react-spring": "^10.0.1",
"slick-carousel": "^1.8.1", "slick-carousel": "^1.8.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80" height="4" viewBox="0 0 80 4" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="4" fill="#6a59ff"/>
<rect x="40" width="40" height="4" fill="#0057b8"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f1f5f9"/>
<rect x="100" y="75" width="200" height="150" fill="#e2e8f0" rx="8"/>
<circle cx="200" cy="120" r="30" fill="#cbd5e1"/>
<rect x="150" y="170" width="100" height="20" fill="#cbd5e1" rx="4"/>
<rect x="170" y="200" width="60" height="15" fill="#cbd5e1" rx="4"/>
</svg>

After

Width:  |  Height:  |  Size: 430 B

View File

@@ -3,13 +3,15 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="Kediri Technopark - Katalis Karir dan Bisnis Digital"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- Font Awesome -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -24,7 +26,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>Kediri Technopark</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -9,8 +9,11 @@ import ServicesSection from './components/ServicesSection';
import ProductSection from './components/ProductSection'; import ProductSection from './components/ProductSection';
import AcademySection from './components/AcademySection'; import AcademySection from './components/AcademySection';
import AboutUsSection from './components/AboutUsSection'; import AboutUsSection from './components/AboutUsSection';
import KnowledgeBaseSection from './components/KnowledgeBaseSection'; // KnowledgeBaseSection hidden temporarily
import ClientsSection from './components/ClientsSection'; // import KnowledgeBaseSection from './components/KnowledgeBaseSection';
// ClientsSection hidden temporarily
// import ClientsSection from './components/ClientsSection';
import FAQSection from './components/FAQSection';
import Footer from './components/Footer'; import Footer from './components/Footer';
import ProductDetailPage from './components/ProductDetailPage'; import ProductDetailPage from './components/ProductDetailPage';
import Dashboard from './components/Dashboard'; import Dashboard from './components/Dashboard';

View File

@@ -1,40 +1,37 @@
import React from 'react'; import React from 'react';
import { Container, Row, Col, Button } from 'react-bootstrap'; import { Container, Row, Col } from 'react-bootstrap';
import styles from './AboutUsSection.module.css';
import { CheckCircle } from 'lucide-react';
import AnimatedBackground from './AnimatedBackground'; // Impor komponen baru
import shared from './Styles.module.css';
import useInView from '../hooks/useInView';
const AboutUsSection = () => { const AboutUsSection = () => {
const { ref, inView } = useInView();
return ( return (
<section id="about" className="about-us section pt-5"> <section id="about" ref={ref} style={{scrollMarginTop: '65px'}} className={`${styles.aboutSection} ${shared.revealSection} ${inView ? shared.isVisible : ''}`}>
<Container> <AnimatedBackground /> {/* Komponen animasi sebagai latar belakang */}
<Row className="align-items-center"> <div className={styles.contentWrapper}>
<Col lg={6}> <Container>
<div className="section-heading"> <Row className="justify-content-center">
<span style={{ color: '#6a59ff', fontWeight: 'bold' }}>Kediri Technopark</span> <Col lg={8}>
<h2 className="mt-2">ABOUT US</h2> <div className={styles.textContent}>
<img src="/assets/images/heading-line-dec.png" alt="" /> <h2 className={styles.sectionTitle}>Tentang Kami</h2>
<p className="mt-3"> <p className={styles.paragraph}>
<strong>Kediri Technopark: Katalis Inovasi dan Pusat Pertumbuhan Digital Lokal</strong><br /><br /> 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.
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 /> </p>
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. <ul className={styles.valueList}>
</p> <li><CheckCircle size={20} className={styles.listIcon} /><span>Inovasi Berkelanjutan</span></li>
<div className="mt-4 d-flex gap-3"> <li><CheckCircle size={20} className={styles.listIcon} /><span>Kolaborasi Komunitas</span></li>
<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' }}> <li><CheckCircle size={20} className={styles.listIcon} /><span>Pemberdayaan Talenta</span></li>
Instagram </ul>
</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>
</div> </div>
</div> </Col>
</Col> </Row>
<Col lg={6}> </Container>
<div className="right-image"> </div>
<img src="/assets/images/about-right-dec.png" alt="" className="img-fluid" />
</div>
</Col>
</Row>
</Container>
</section> </section>
); );
}; };
export default AboutUsSection; export default AboutUsSection;

View File

@@ -0,0 +1,60 @@
.aboutSection {
padding: 80px 0; /* Ditinggikan dari 60px */
position: relative;
background-color: #0f172a; /* Latar belakang biru gelap (slate-900) */
}
/* Menghapus .blueprintGrid */
.contentWrapper {
position: relative;
z-index: 1;
}
.textContent {
text-align: center;
}
.sectionTitle {
font-family: 'Inter', sans-serif;
font-weight: 700;
font-size: clamp(1.8rem, 4vw, 2.2rem);
color: #ffffff; /* Warna teks putih */
letter-spacing: -0.02em;
margin-bottom: 16px;
}
.paragraph {
font-family: 'Inter', sans-serif;
font-size: 1.1rem;
line-height: 1.7;
color: #cbd5e1; /* Warna teks abu-abu terang (slate-300) */
margin-bottom: 24px;
max-width: 700px;
margin-left: auto;
margin-right: auto;
}
.valueList {
list-style: none;
padding: 0;
margin: 0;
display: flex;
justify-content: center;
gap: 24px;
flex-wrap: wrap;
}
.valueList li {
display: flex;
align-items: center;
gap: 10px;
font-family: 'Inter', sans-serif;
font-size: 1.05rem;
color: #f1f5f9; /* Warna teks putih keabuan (slate-100) */
}
.listIcon {
color: #38bdf8; /* Warna ikon biru cerah (sky-400) */
flex-shrink: 0;
}

View File

@@ -1,32 +1,60 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Container } from 'react-bootstrap'; import { Container } from 'react-bootstrap';
import styles from './Styles.module.css'; import styles from './Styles.module.css';
import useInView from '../hooks/useInView';
const AcademySection = ({setSelectedProduct, setShowedModal, courseSectionRef, setWillDo}) => { const AcademySection = ({setSelectedProduct, setShowedModal, courseSectionRef, setWillDo}) => {
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
const [hoveredCard, setHoveredCard] = useState(null); const [hoveredCard, setHoveredCard] = useState(null);
useEffect(() => { useEffect(() => {
// Fetch all items to compute module/sessions reliably, then filter courses client-side
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', { fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ type: 'course' }), body: JSON.stringify({}),
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => setProducts(data)) .then(data => {
const all = Array.isArray(data) ? data : [];
const moduleCountMap = {};
const sessionsCountMap = {};
all.forEach(item => {
const parentId = item?.sub_product_of;
if (parentId) {
moduleCountMap[parentId] = (moduleCountMap[parentId] || 0) + 1;
const s = Number(item?.sessions || item?.session_count || 0);
sessionsCountMap[parentId] = (sessionsCountMap[parentId] || 0) + (isNaN(s) ? 0 : s);
}
});
const coursesOnly = all.filter(p => (p?.type || '').toLowerCase() === 'course');
const enriched = coursesOnly.map(p => ({
...p,
module_count: p?.module_count ?? p?.modules ?? moduleCountMap[p.id] ?? 0,
session_count: p?.session_count ?? p?.sessions ?? sessionsCountMap[p.id] ?? 0,
}));
setProducts(enriched);
})
.catch(err => console.error('Fetch error:', err)); .catch(err => console.error('Fetch error:', err));
}, []); }, []);
const { ref, inView } = useInView();
return ( return (
<section id="services" className="services pt-5" ref={courseSectionRef}> <section id="academy" style={{scrollMarginTop: '65px' }} className={`services pt-5 ${styles.academySection} ${styles.revealSection} ${inView ? styles.isVisible : ''}`} ref={(el) => {
if (typeof courseSectionRef === 'function') courseSectionRef(el);
if (ref) ref.current = el;
}}>
<Container> <Container>
<div className="section-heading mb-4"> <div className={styles.sectionHeader}>
<h4>OUR <em>ACADEMY PROGRAM</em></h4> <div className={styles.sectionEyebrow}>Academy</div>
<img src="/assets/images/heading-line-dec.png" alt="" /> <h2 className={styles.sectionTitle}>Our Academy Program</h2>
<p>Academy Program adalah wadah belajar digital untuk anak-anak dan remaja. Didesain interaktif, kreatif, dan gratis setiap modul membekali peserta dengan keterampilan masa depan, dari teknologi dasar hingga coding dan proyek nyata.</p> <div className={styles.sectionRule} />
<p className={styles.sectionSubtitle}>
Academy Program adalah wadah belajar digital untuk anak-anak dan remaja. Didesain interaktif, kreatif, dan gratis setiap modul membekali peserta dengan keterampilan masa depan, dari teknologi dasar hingga coding dan proyek nyata.
</p>
</div> </div>
<div className={styles.coursesGrid}> <div className={styles.coursesGrid}>
@@ -51,7 +79,14 @@ const AcademySection = ({setSelectedProduct, setShowedModal, courseSectionRef, s
)} )}
</div> </div>
<div className={styles.courseContentTop}> <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> <p className={styles.courseDesc}>{product.description}</p>
</div> </div>
</div> </div>
@@ -69,12 +104,12 @@ const AcademySection = ({setSelectedProduct, setShowedModal, courseSectionRef, s
: `Rp ${product.price.toLocaleString('id-ID')}`} : `Rp ${product.price.toLocaleString('id-ID')}`}
</span> </span>
</div> </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={() => { onClick={() => {
setSelectedProduct(product); setSelectedProduct(product);
setShowedModal('product'); setShowedModal('product');
setWillDo('checkout'); setWillDo('checkout');
}}>Beli</button> }}>Daftar</button>
</div> </div>
</div> </div>
))} ))}

View File

@@ -0,0 +1,161 @@
import React, { useRef, useEffect } from 'react';
import styles from './AnimatedBackground.module.css';
const AnimatedBackground = () => {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
let animationFrameId;
let particles = [];
// Determine particle count based on screen size
const getParticleCount = () => {
const width = window.innerWidth;
if (width <= 400) return 30; // Very small screens
if (width <= 576) return 40; // Small screens
if (width <= 768) return 50; // Medium screens
return 70; // Large screens
};
function Particle(x, y, vx, vy) {
this.x = x; this.y = y; this.vx = vx; this.vy = vy;
// Adjust particle radius based on screen size
if (window.innerWidth <= 400) {
this.radius = 1.0; // Smaller radius for very small screens
} else if (window.innerWidth <= 576) {
this.radius = 1.2; // Medium radius for small screens
} else {
this.radius = 1.5; // Default radius for larger screens
}
}
const setupCanvas = () => {
const parent = canvas.parentElement;
if (!parent) return;
const dpr = window.devicePixelRatio || 1;
const rect = parent.getBoundingClientRect();
const cssWidth = parent.clientWidth;
const cssHeight = parent.clientHeight;
canvas.width = cssWidth * dpr;
canvas.height = cssHeight * dpr;
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;
ctx.scale(dpr, dpr);
// Get particle count based on current screen size
const particleCount = getParticleCount();
particles = [];
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle(Math.random() * cssWidth, Math.random() * cssHeight, (Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.5));
}
};
function connectParticles() {
const cssWidth = canvas.clientWidth;
const cssHeight = canvas.clientHeight;
// Adjust connection distance based on screen size
let connectionDistance = 90;
if (window.innerWidth <= 400) {
connectionDistance = 60; // Smaller connection distance for very small screens
} else if (window.innerWidth <= 576) {
connectionDistance = 70; // Medium connection distance for small screens
}
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const p1 = particles[i];
const p2 = particles[j];
// Array posisi p2, termasuk "hantu" di 8 sisi lain
const p2Positions = [
{ x: p2.x, y: p2.y }, // Posisi asli
{ x: p2.x + cssWidth, y: p2.y }, // Kanan
{ x: p2.x - cssWidth, y: p2.y }, // Kiri
{ x: p2.x, y: p2.y + cssHeight }, // Bawah
{ x: p2.x, y: p2.y - cssHeight }, // Atas
{ x: p2.x + cssWidth, y: p2.y + cssHeight }, // Kanan-bawah
{ x: p2.x - cssWidth, y: p2.y - cssHeight }, // Kiri-atas
{ x: p2.x + cssWidth, y: p2.y - cssHeight }, // Kanan-atas
{ x: p2.x - cssWidth, y: p2.y + cssHeight } // Kiri-bawah
];
// Cari jarak terpendek ke p2 atau salah satu hantunya
for (const pos of p2Positions) {
const distance = Math.sqrt((p1.x - pos.x) ** 2 + (p1.y - pos.y) ** 2);
if (distance < connectionDistance) {
ctx.strokeStyle = `rgba(255, 255, 255, ${1 - distance / connectionDistance})`;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(pos.x, pos.y); // Gambar garis ke posisi terdekat (bisa jadi hantu)
ctx.stroke();
break; // Hanya gambar satu garis terpendek
}
}
}
}
}
function animate() {
const cssWidth = canvas.clientWidth;
const cssHeight = canvas.clientHeight;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Adjust animation speed based on screen size
let speedFactor = 1.0;
if (window.innerWidth <= 400) {
speedFactor = 0.7; // Slower animation for very small screens
} else if (window.innerWidth <= 576) {
speedFactor = 0.8; // Slightly slower animation for small screens
}
for (const p of particles) {
// LOGIKA BARU: Partikel tembus (wrapping) bukan memantul (bounce)
if (p.x > cssWidth + p.radius) p.x = -p.radius;
else if (p.x < -p.radius) p.x = cssWidth + p.radius;
if (p.y > cssHeight + p.radius) p.y = -p.radius;
else if (p.y < -p.radius) p.y = cssHeight + p.radius;
p.x += p.vx * speedFactor;
p.y += p.vy * speedFactor;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fill();
}
connectParticles();
animationFrameId = requestAnimationFrame(animate);
}
setupCanvas();
animate();
// Handle resize events to adjust particle count
const handleResize = () => {
setupCanvas();
};
window.addEventListener('resize', handleResize);
return () => {
cancelAnimationFrame(animationFrameId);
window.removeEventListener('resize', handleResize);
};
}, []);
return <canvas ref={canvasRef} className={styles.particleCanvas}></canvas>;
};
export default AnimatedBackground;

View File

@@ -0,0 +1,7 @@
.particleCanvas {
position: absolute;
top: 0;
left: 0;
z-index: 0;
/* Ukuran akan diatur oleh JavaScript */
}

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Container, Row, Col, Image } from 'react-bootstrap'; import { Container, Row, Col, Image } from 'react-bootstrap';
import styles from './Styles.module.css'; import styles from './Styles.module.css';
import useInView from '../hooks/useInView';
const ClientsSection = () => { const ClientsSection = () => {
const logos = [ const logos = [
@@ -10,8 +11,9 @@ const ClientsSection = () => {
'psi.png', 'psi.png',
]; ];
const { ref, inView } = useInView();
return ( 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> <Container>
<Row> <Row>
<Col> <Col>

View File

@@ -0,0 +1,225 @@
import React, { useState, useEffect, useRef } from 'react';
import styles from './CoverflowCarousel.module.css';
import ProductCard from './ProductCard';
const CoverflowCarousel = ({ products, onCardClick }) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [animationState, setAnimationState] = useState('initial'); // 'initial', 'spread', 'ready'
const [shiftDirection, setShiftDirection] = useState(null); // 'left' | 'right' | null
const [isDragging, setIsDragging] = useState(false);
const [edgeEnter, setEdgeEnter] = useState(null); // 'left' | 'right' | null
const containerRef = useRef(null);
const dragStartX = useRef(null);
const hasSwiped = useRef(false);
// Handle navigation
const goToProduct = (index, dir = null) => {
if (dir) {
setShiftDirection(dir);
// mark which edge is entering for a short time to trigger ease-in
setEdgeEnter(dir === 'left' ? 'right' : 'left');
setTimeout(() => setEdgeEnter(null), 80);
setTimeout(() => setShiftDirection(null), 820);
}
setCurrentIndex(index);
};
const nextProduct = () => {
const nextIndex = (currentIndex + 1) % products.length;
goToProduct(nextIndex, 'right');
};
const prevProduct = () => {
const prevIndex = (currentIndex - 1 + products.length) % products.length;
goToProduct(prevIndex, 'left');
};
// Drag/Swipe handlers
const onPointerDown = (e) => {
const x = e.touches ? e.touches[0].clientX : e.clientX;
dragStartX.current = x;
hasSwiped.current = false;
setIsDragging(true);
};
const onPointerMove = (e) => {
if (!isDragging) return;
const x = e.touches ? e.touches[0].clientX : e.clientX;
const dx = x - (dragStartX.current ?? x);
const threshold = 60;
if (!hasSwiped.current && Math.abs(dx) > threshold) {
if (dx > 0) {
prevProduct();
} else {
nextProduct();
}
hasSwiped.current = true;
dragStartX.current = x;
}
};
const onPointerUp = () => {
setIsDragging(false);
dragStartX.current = null;
hasSwiped.current = false;
};
// Handle dot navigation
const goToProductByIndex = (index) => {
if (index === currentIndex) return;
const dir = index > currentIndex ? 'right' : 'left';
goToProduct(index, dir);
};
// Collapse overlay for center card
const collapseOverlay = () => {
// Reset animation state to force collapse
setAnimationState('spread');
setTimeout(() => {
setAnimationState('ready');
}, 50);
};
// Initialize carousel with spread effect when products are available
useEffect(() => {
if (!products || products.length === 0) return;
setAnimationState('initial');
const spreadTimer = setTimeout(() => {
setAnimationState('spread');
}, 100);
const readyTimer = setTimeout(() => {
setAnimationState('ready');
}, 1000);
return () => {
clearTimeout(spreadTimer);
clearTimeout(readyTimer);
};
}, [products.length]);
// Keyboard navigation
useEffect(() => {
const onKey = (e) => {
if (e.key === 'ArrowLeft') {
prevProduct();
} else if (e.key === 'ArrowRight') {
nextProduct();
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [currentIndex, products.length]);
// Don't render if no products
if (products.length === 0) {
return <div className={styles.container}>Tidak ada produk tersedia</div>;
}
// Prefer 5-slot coverflow when data memadai; fallback ke jumlah ganjil saat produk < 5
let positions = [];
if (products.length >= 5) {
positions = [-2, -1, 0, 1, 2];
} else {
const visibleCountRaw = Math.min(5, products.length);
const visibleCount = visibleCountRaw % 2 === 0 ? Math.max(1, visibleCountRaw - 1) : visibleCountRaw;
const half = Math.floor(visibleCount / 2);
positions = Array.from({ length: visibleCount }, (_, i) => i - half);
}
return (
<div
ref={containerRef}
className={`${styles.container} ${isDragging ? styles.dragging : ''}`}
onMouseDown={onPointerDown}
onMouseMove={onPointerMove}
onMouseUp={onPointerUp}
onMouseLeave={onPointerUp}
onTouchStart={onPointerDown}
onTouchMove={onPointerMove}
onTouchEnd={onPointerUp}
>
<div className={`${styles.carouselWrapper} ${shiftDirection === 'left' ? styles.shiftingLeft : ''} ${shiftDirection === 'right' ? styles.shiftingRight : ''}`}>
{positions.map((position) => {
const count = products.length;
const productIndex = (currentIndex + position + count) % count;
const product = products[productIndex];
// Determine position class (clamped to available classes)
let positionClass = '';
if (position <= -2) positionClass = styles.positionNeg2;
else if (position === -1) positionClass = styles.positionNeg1;
else if (position === 0) positionClass = styles.position0;
else if (position === 1) positionClass = styles.position1;
else if (position >= 2) positionClass = styles.position2;
// Determine entering class for edge items (works for 3 or 5 slots)
const maxEdge = positions.length > 0 ? Math.max(...positions) : 2; // 2 for 5 slots, 1 for 3 slots
const enteringClass = edgeEnter === 'right' && position === maxEdge
? styles.enterFromRight
: edgeEnter === 'left' && position === -maxEdge
? styles.enterFromLeft
: '';
return (
<div
key={`prod_${(product && product.id != null) ? product.id : productIndex}`}
className={`${styles.cardContainer} ${positionClass} ${
animationState === 'initial' ? styles.initial :
animationState === 'spread' ? styles.spread : ''
}`}
onClick={() => {
// Only trigger navigation if this is not the center card
// or if it's the center card but not in hover state (overlay not visible)
const isCenter = position === 0;
const canHover = isCenter && animationState === 'ready' && !shiftDirection && !isDragging;
if (position !== 0 || (position === 0 && (!canHover || animationState !== 'ready' || shiftDirection || isDragging))) {
goToProduct(productIndex, position > 0 ? 'right' : (position < 0 ? 'left' : null));
}
}}
>
<div className={styles.cardShadow} aria-hidden="true"></div>
<div className={styles.cardWrapper}>
<ProductCard
product={product}
onCardClick={(p) => { onCardClick && onCardClick(p); }}
isCenter={position === 0}
canHover={position === 0 && animationState === 'ready' && !shiftDirection && !isDragging}
onCollapse={position === 0 ? collapseOverlay : undefined}
/>
</div>
</div>
);
})}
</div>
{/* Navigation buttons */}
<button className={styles.navButton} onClick={prevProduct}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 18L9 12L15 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
<button className={styles.navButton} onClick={nextProduct}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18L15 12L9 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
{/* Dots indicator */}
{products.length > 1 && (
<div className={styles.dotsContainer}>
{products.map((_, index) => (
<button
key={index}
className={`${styles.dot} ${index === currentIndex ? styles.active : ''}`}
onClick={() => goToProductByIndex(index)}
/>
))}
</div>
)}
</div>
);
};
export default CoverflowCarousel;

View File

@@ -0,0 +1,655 @@
.container {
position: relative;
width: 100%;
height: 450px;
overflow: hidden;
touch-action: none;
margin: 40px 0 60px 0;
perspective: 1000px;
cursor: grab;
user-select: none;
}
/* Left and right fade out masks */
.leftMask,
.rightMask {
position: absolute;
top: 0;
bottom: 0;
width: 100px;
z-index: 20;
pointer-events: none;
}
.leftMask {
left: 0;
background: linear-gradient(to right, #0b1220, transparent);
}
.rightMask {
right: 0;
background: linear-gradient(to left, #0b1220, transparent);
}
.container.dragging {
cursor: grabbing;
}
.carouselWrapper {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transform-style: preserve-3d;
perspective: 1500px;
}
/* Entering edge helpers to ensure ease-out even with <5 products */
/* entering helpers removed (reverted) */
/* Edge fade-out on shift for better intuition */
.carouselWrapper.shiftingLeft .cardContainer.positionNeg2 {
opacity: 0;
transform: translateX(-420px) rotateY(62deg) scale(0.66);
}
.carouselWrapper.shiftingRight .cardContainer.position2 {
opacity: 0;
transform: translateX(420px) rotateY(-62deg) scale(0.66);
}
.cardContainer {
position: absolute;
width: 320px;
height: 370px;
cursor: pointer;
transform-style: preserve-3d;
will-change: transform;
backface-visibility: hidden;
transform-origin: center center;
transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1);
transform: translateX(0) rotateY(0) scale(1);
opacity: 1;
z-index: 1;
box-sizing: border-box;
aspect-ratio: 320/370;
}
.cardWrapper {
width: 100%;
height: 100%;
padding: 0 15px;
transform-style: preserve-3d;
backface-visibility: hidden;
box-sizing: border-box;
position: relative;
z-index: 2; /* ensure content stacks above shadow */
}
/* Ground shadow to enhance 3D standing effect */
.cardShadow {
position: absolute;
left: 50%;
bottom: 6px;
width: 92%;
height: 32px;
transform: translateX(-50%) scale(1);
background: radial-gradient(ellipse at center, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.32) 45%, rgba(0,0,0,0) 78%);
filter: blur(8px);
opacity: 0.45;
pointer-events: none;
z-index: 1;
}
.cardContainer.position0 .cardShadow {
opacity: 0.8;
transform: translateX(-50%) scale(1.15);
}
.cardContainer.positionNeg1 .cardShadow,
.cardContainer.position1 .cardShadow {
opacity: 0.55;
transform: translateX(-50%) scale(1.02);
}
.cardContainer.positionNeg2 .cardShadow,
.cardContainer.position2 .cardShadow {
opacity: 0.42;
transform: translateX(-50%) scale(0.96);
}
/* Initial state: subtle pop-in */
.cardContainer.initial {
transform: translateX(0) translateY(20px) rotateY(0) scale(0.92);
opacity: 0;
z-index: 1;
}
/* New load spread: compact fan-out from center */
.cardContainer.spread.positionNeg2 {
transform: translateX(-320px) translateZ(-120px) rotateY(10deg) scale(0.9);
opacity: 0.8;
}
.cardContainer.spread.positionNeg1 {
transform: translateX(-160px) translateZ(-60px) rotateY(6deg) scale(0.97);
opacity: 0.9;
}
.cardContainer.spread.position0 {
transform: translateX(0) translateZ(0) rotateY(0) scale(1.06);
opacity: 1;
}
.cardContainer.spread.position1 {
transform: translateX(160px) translateZ(-60px) rotateY(-6deg) scale(0.97);
opacity: 0.9;
}
.cardContainer.spread.position2 {
transform: translateX(320px) translateZ(-120px) rotateY(-10deg) scale(0.9);
opacity: 0.8;
}
/* Position classes for coverflow effect (closer to center) */
.cardContainer.positionNeg2 {
transform: translateX(-380px) rotateY(55deg) scale(0.7);
opacity: 1;
z-index: 5;
}
.cardContainer.positionNeg1 {
transform: translateX(-190px) rotateY(35deg) scale(0.8);
opacity: 1;
z-index: 10;
}
.cardContainer.position0 {
transform: translateX(0) rotateY(0deg) scale(1);
opacity: 1;
z-index: 15;
}
.cardContainer.position1 {
transform: translateX(190px) rotateY(-35deg) scale(0.8);
opacity: 1;
z-index: 10;
}
.cardContainer.position2 {
transform: translateX(380px) rotateY(-55deg) scale(0.7);
opacity: 1;
z-index: 5;
}
/* Edge-enter overrides (placed after position rules to win cascade) */
.cardContainer.enterFromRight.position1,
.cardContainer.enterFromRight.position2 {
opacity: 0;
transform: translateX(520px) rotateY(-62deg) scale(0.66);
}
.cardContainer.enterFromLeft.positionNeg1,
.cardContainer.enterFromLeft.positionNeg2 {
opacity: 0;
transform: translateX(-520px) rotateY(62deg) scale(0.66);
}
.cardImage {
width: 100%;
height: 120px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transition: transform 0.4s ease;
position: relative;
background-color: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
object-fit: cover;
aspect-ratio: 4/3;
overflow: hidden;
}
.placeholderImage {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: #f1f5f9;
object-fit: cover;
aspect-ratio: 4/3;
overflow: hidden;
}
.placeholderImage span {
font-size: 2rem;
opacity: 0.3;
}
.cardContent {
padding: 1rem;
flex: 1;
display: flex;
flex-direction: column;
}
.cardTitle {
font-size: 1.1rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 0.5rem;
line-height: 1.3;
flex: 1;
}
.cardDescription {
color: #64748b;
line-height: 1.5;
margin-bottom: 1rem;
font-size: 0.85rem;
flex: 1;
}
.cardFooter {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #e2e8f0;
padding-top: 1rem;
margin-top: auto;
}
.priceContainer {
flex: 1;
}
.price {
font-size: 1rem;
font-weight: 700;
}
.freePrice {
color: #059669;
}
.paidPrice {
color: #2563eb;
}
.enrollButton {
background: linear-gradient(135deg, #6a59ff 0%, #8261ee 100%);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(106, 89, 255, 0.2);
flex-shrink: 0;
}
.enrollButton:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(106, 89, 255, 0.3);
}
.enrollButton:active {
transform: translateY(0);
}
.noCourses {
text-align: center;
grid-column: 1 / -1;
padding: 3rem;
color: #64748b;
}
.loadingCourses {
text-align: center;
grid-column: 1 / -1;
padding: 3rem;
color: #64748b;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.loadingSpinner {
width: 24px;
height: 24px;
border: 3px solid #e2e8f0;
border-top: 3px solid #6a59ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Navigation buttons */
.navButton {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
border-radius: 50%;
background-color: white;
border: 2px solid #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
color: #475569;
z-index: 100;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.navButton:hover:not(:disabled) {
border-color: #6a59ff;
color: #6a59ff;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.navButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.navButton:first-of-type {
left: 30px;
}
.navButton:last-of-type {
right: 30px;
}
/* Dots indicator */
.dotsContainer {
position: absolute;
bottom: -40px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 12px;
z-index: 100;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #cbd5e1;
border: none;
cursor: pointer;
transition: all 0.3s ease;
transform: scale(1);
}
.dot.active {
background-color: #6a59ff;
transform: scale(1.2);
}
.dot:hover {
background-color: #94a3b8;
}
/* Responsive design */
@media (max-width: 1200px) {
.container {
height: 430px;
margin: 35px 0 55px 0;
}
.cardContainer {
width: 300px;
height: 350px;
}
.navButton {
width: 45px;
height: 45px;
}
.navButton:first-of-type {
left: 25px;
}
.navButton:last-of-type {
right: 25px;
}
.dotsContainer {
bottom: -35px;
}
.leftMask,
.rightMask {
width: 80px;
}
}
@media (max-width: 992px) {
.container {
height: 410px;
margin: 30px 0 50px 0;
}
.cardContainer {
width: 280px;
height: 330px;
}
.cardWrapper {
padding: 0 12px;
}
.navButton {
width: 40px;
height: 40px;
}
.navButton:first-of-type {
left: 20px;
}
.navButton:last-of-type {
right: 20px;
}
.dotsContainer {
bottom: -30px;
}
.dot {
width: 10px;
height: 10px;
}
.leftMask,
.rightMask {
width: 70px;
}
}
@media (max-width: 768px) {
.container {
height: 380px;
margin: 25px 0 45px 0;
}
.cardContainer {
width: 250px;
height: 300px;
}
.cardWrapper {
padding: 0 10px;
}
.navButton {
width: 35px;
height: 35px;
}
.navButton:first-of-type {
left: 15px;
}
.navButton:last-of-type {
right: 15px;
}
.navButton svg {
width: 20px;
height: 20px;
}
.dotsContainer {
bottom: -25px;
gap: 10px;
}
.dot {
width: 9px;
height: 9px;
}
.leftMask,
.rightMask {
width: 60px;
}
}
@media (max-width: 576px) {
.container {
height: 320px;
margin: 20px 0 40px 0;
}
.cardContainer {
width: 200px;
height: 250px;
}
.cardWrapper {
padding: 0 6px;
}
.navButton {
width: 30px;
height: 30px;
}
.navButton:first-of-type {
left: 10px;
}
.navButton:last-of-type {
right: 10px;
}
.navButton svg {
width: 16px;
height: 16px;
}
.dotsContainer {
bottom: -20px;
gap: 8px;
}
.dot {
width: 8px;
height: 8px;
}
/* Adjust positions for smaller screens */
.cardContainer.positionNeg2 {
transform: translateX(-280px) rotateY(55deg) scale(0.6);
}
.cardContainer.positionNeg1 {
transform: translateX(-140px) rotateY(35deg) scale(0.75);
}
.cardContainer.position1 {
transform: translateX(140px) rotateY(-35deg) scale(0.75);
}
.cardContainer.position2 {
transform: translateX(280px) rotateY(-55deg) scale(0.6);
}
.leftMask,
.rightMask {
width: 50px;
}
}
@media (max-width: 400px) {
.container {
height: 280px;
margin: 15px 0 35px 0;
}
.cardContainer {
width: 180px;
height: 230px;
}
.navButton {
width: 28px;
height: 28px;
}
.navButton svg {
width: 14px;
height: 14px;
}
.dotsContainer {
bottom: -18px;
}
.dot {
width: 7px;
height: 7px;
}
/* Further adjust positions for very small screens */
.cardContainer.positionNeg2 {
transform: translateX(-240px) rotateY(55deg) scale(0.5);
}
.cardContainer.positionNeg1 {
transform: translateX(-120px) rotateY(35deg) scale(0.7);
}
.cardContainer.position1 {
transform: translateX(120px) rotateY(-35deg) scale(0.7);
}
.cardContainer.position2 {
transform: translateX(240px) rotateY(-55deg) scale(0.5);
}
.leftMask,
.rightMask {
width: 40px;
}
}

View File

@@ -0,0 +1,160 @@
import React, { useState } from 'react';
import { Container, Row, Col } from 'react-bootstrap';
import styles from './FAQSection.module.css';
const Accordion = ({ children, type = "single", collapsible = true, className = "" }) => {
return (
<div className={`${styles.accordion} ${className}`}>
{children}
</div>
);
};
const AccordionItem = ({ children, value, className = "" }) => {
return (
<div className={`${styles.accordionItem} ${className}`} data-value={value}>
{children}
</div>
);
};
const AccordionTrigger = ({ children, className = "", onClick, isExpanded }) => {
return (
<button
className={`${styles.accordionTrigger} ${className} ${isExpanded ? styles.expanded : ''}`}
onClick={onClick}
aria-expanded={isExpanded}
>
<span className={styles.triggerText}>{children}</span>
<svg
className={`${styles.chevron} ${isExpanded ? styles.rotated : ''}`}
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M4 6L8 10L12 6"
stroke={isExpanded ? "#0057b8" : "currentColor"}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
);
};
const AccordionContent = ({ children, className = "", isExpanded }) => {
return (
<div
className={`${styles.accordionContent} ${className} ${isExpanded ? styles.expanded : ''}`}
style={{
maxHeight: isExpanded ? '500px' : '0',
opacity: isExpanded ? 1 : 0,
}}
>
<div className={styles.contentInner}>
{children}
</div>
</div>
);
};
const FAQSection = () => {
const [activeItem, setActiveItem] = useState("item-1");
const handleToggle = (value) => {
if (activeItem === value) {
setActiveItem("");
} else {
setActiveItem(value);
}
};
const faqs = [
{
id: "item-1",
question: "Apa itu Kediri Technopark?",
answer: "Kediri Technopark adalah pusat pengembangan inovasi digital dan aplikasi untuk masyarakat dan pelaku usaha di Kediri. Kami menyediakan berbagai solusi teknologi untuk membantu bisnis berkembang di era digital."
},
{
id: "item-2",
question: "Produk apa saja yang ditawarkan oleh Kediri Technopark?",
answer: "Kami menawarkan berbagai produk digital seperti platform Point of Sale (Kedai Master), aplikasi manajemen bisnis, solusi e-commerce, serta layanan pengembangan website dan aplikasi custom sesuai kebutuhan bisnis Anda."
},
{
id: "item-3",
question: "Apakah ada program akademi untuk belajar teknologi?",
answer: "Ya, kami memiliki Academy Program yang dirancang untuk anak-anak dan remaja. Program ini mencakup berbagai bidang seperti pemrograman, robotika, desain grafis, pengembangan web, dan data science dengan pendekatan yang interaktif dan kreatif."
},
{
id: "item-4",
question: "Bagaimana cara mendaftar program akademi?",
answer: "Anda dapat mendaftar melalui website kami dengan mengklik tombol 'Daftar' pada program yang diminati. Setelah itu, tim kami akan menghubungi Anda untuk proses selanjutnya. Beberapa program bahkan tersedia secara gratis."
},
{
id: "item-5",
question: "Apakah ada biaya untuk menggunakan produk Kediri Technopark?",
answer: "Kami menawarkan berbagai paket dengan harga yang berbeda sesuai dengan kebutuhan bisnis Anda. Beberapa produk memiliki versi gratis dengan fitur dasar, dan paket berbayar dengan fitur yang lebih lengkap. Anda dapat melihat detail harga di halaman produk masing-masing."
},
{
id: "item-6",
question: "Berapa lama waktu implementasi produk Kediri Technopark?",
answer: "Waktu implementasi tergantung pada kompleksitas kebutuhan bisnis Anda. Untuk produk standar seperti Kedai Master, implementasi bisa dilakukan dalam 1-3 hari kerja. Untuk solusi custom, waktu implementasi akan disesuaikan dengan kebutuhan spesifik Anda."
},
{
id: "item-7",
question: "Apakah tersedia pelatihan penggunaan produk?",
answer: "Ya, kami menyediakan pelatihan gratis untuk penggunaan produk standar kami. Untuk paket berbayar, pelatihan akan disesuaikan dengan paket yang Anda pilih. Tim support kami juga selalu siap membantu jika Anda memiliki pertanyaan."
},
{
id: "item-8",
question: "Bagaimana jika saya memiliki pertanyaan teknis?",
answer: "Anda dapat menghubungi tim support kami melalui email marketing@kediritechnopark.com atau melalui nomor WhatsApp 0813 1889 4994. Tim kami akan dengan senang hati membantu menjawab pertanyaan teknis Anda."
}
];
return (
<section id="faq" style={{scrollMarginTop: '65px'}} className={styles.faqSection}>
<Container>
<Row>
<Col>
<div className={styles.sectionHeading}>
<h2 className={styles.sectionTitle}>
FREQUENTLY ASKED <span className={styles.highlight}>QUESTIONS</span>
</h2>
<p className={styles.sectionDescription}>
Temukan jawaban untuk pertanyaan umum tentang layanan dan produk kami
</p>
</div>
<Accordion type="single" collapsible className={styles.accordionRoot}>
{faqs.map((faq) => (
<AccordionItem key={faq.id} value={faq.id} className={styles.accordionItemShadcn}>
<AccordionTrigger
isExpanded={activeItem === faq.id}
onClick={() => handleToggle(faq.id)}
className={styles.accordionTriggerShadcn}
>
{faq.question}
</AccordionTrigger>
<AccordionContent
isExpanded={activeItem === faq.id}
className={styles.accordionContentShadcn}
>
<div className={styles.answer}>
{faq.answer}
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</Col>
</Row>
</Container>
</section>
);
};
export default FAQSection;

View File

@@ -0,0 +1,214 @@
.faqSection {
padding: 80px 20px;
background-color: #ffffff;
}
.sectionHeading {
text-align: center;
margin-bottom: 50px;
}
.sectionTitle {
font-size: 2.5rem;
font-weight: 800;
color: #0f172a;
margin-bottom: 15px;
letter-spacing: -0.02em;
}
.highlight {
color: #0057b8;
}
.sectionDescription {
font-size: 1.1rem;
color: #64748b;
max-width: 600px;
margin: 0 auto;
line-height: 1.7;
}
.accordionRoot {
max-width: 800px;
margin: 0 auto;
border-radius: 8px;
overflow: hidden;
}
.accordionItemShadcn {
border-radius: 0;
border-bottom: 1px solid #e2e8f0;
background-color: white;
transition: all 0.2s ease;
}
.accordionItemShadcn:last-child {
border-bottom: none;
}
.accordionItemShadcn:hover {
background-color: #f8fafc;
}
.accordionTriggerShadcn {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 20px;
width: 100%;
background-color: transparent;
border: none;
text-align: left;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
color: #0f172a;
transition: all 0.2s ease;
}
.accordionTriggerShadcn:hover {
background-color: #f1f5f9;
}
.accordionTriggerShadcn:focus-visible {
outline: 2px solid #0057b8;
outline-offset: -2px;
border-radius: 4px;
}
.triggerText {
flex: 1;
margin-right: 16px;
line-height: 1.4;
}
.chevron {
color: #94a3b8;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.chevron.rotated {
transform: rotate(180deg);
}
.accordionContentShadcn {
overflow: hidden;
transition: max-height 0.2s ease, opacity 0.2s ease;
background-color: white;
}
.accordionContentShadcn.expanded {
border-top: 1px solid #e2e8f0;
}
.contentInner {
padding: 15px 20px 20px;
}
.answer {
color: #64748b;
line-height: 1.6;
font-size: 0.95rem;
}
/* Responsive design */
@media (max-width: 1200px) {
.faqSection {
padding: 70px 20px;
}
.sectionTitle {
font-size: 2.2rem;
}
}
@media (max-width: 992px) {
.faqSection {
padding: 60px 20px;
}
.sectionTitle {
font-size: 2rem;
}
.sectionDescription {
font-size: 1rem;
}
.accordionTriggerShadcn {
padding: 16px 18px;
font-size: 0.95rem;
}
.contentInner {
padding: 0 18px 18px;
}
.answer {
font-size: 0.9rem;
}
}
@media (max-width: 768px) {
.faqSection {
padding: 50px 15px;
}
.sectionHeading {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 1.8rem;
}
.accordionTriggerShadcn {
padding: 15px 16px;
}
.contentInner {
padding: 0 16px 16px;
}
}
@media (max-width: 576px) {
.faqSection {
padding: 40px 15px;
}
.sectionTitle {
font-size: 1.6rem;
}
.sectionDescription {
font-size: 0.95rem;
}
.accordionTriggerShadcn {
padding: 14px 15px;
font-size: 0.9rem;
}
.contentInner {
padding: 0 15px 15px;
}
.answer {
font-size: 0.85rem;
}
}
@media (max-width: 400px) {
.faqSection {
padding: 35px 10px;
}
.sectionTitle {
font-size: 1.5rem;
}
.accordionTriggerShadcn {
padding: 12px 14px;
}
}

View File

@@ -1,32 +1,73 @@
import React from 'react'; import React from 'react';
import { Container, Row, Col } from 'react-bootstrap'; import { Container, Row, Col } from 'react-bootstrap';
import styles from './Styles.module.css'; import styles from './Footer.module.css';
const Footer = () => { const Footer = () => {
return ( return (
<footer id="contact" className={`bg-dark text-white py-4 ${styles.footer}`}> <footer className={styles.footer}>
<Container> <Container>
<Row className="justify-content-center text-start"> <Row className={styles.footerContent}>
<Col lg={6} className="mb-3"> <Col lg={3} md={6} className={styles.footerColumn}>
<h4>Contact Us</h4> <div className={styles.footerLogo}>
<p>Sunan Giri GG. I No. 11, Rejomulyo, Kediri, Jawa Timur 64129</p> <img
<p><a href="tel:+6281318894994">0813 1889 4994</a></p> src="https://kediritechnopark.com/kediri-technopark-logo-white.png"
<p><a href="mailto:marketing@kediritechnopark.com">marketing@kediritechnopark.com</a></p> alt="Kediri Technopark Logo"
<p><a href="https://instagram.com/kediri.technopark" target="_blank" rel="noopener noreferrer">@kediri.technopark</a></p> className={styles.logoImage}
<p><a href="https://kediritechnopark.com" target="_blank" rel="noopener noreferrer">www.KEDIRITECHNOPARK.com</a></p> />
</div>
</Col> <p className={styles.companyDescription}>
<Col lg={6}> Kediri Technopark adalah pusat pengembangan inovasi digital dan aplikasi untuk masyarakat dan pelaku usaha.
<div className="footer-widget"> </p>
<h4>About Our Company</h4> <div className={styles.socialLinks}>
<div className={styles.logo}> <a href="https://instagram.com/kediri.technopark" target="_blank" rel="noopener noreferrer" className={styles.socialLink}>
<img src="https://kediritechnopark.com/kediri-technopark-logo-white.png" alt="Logo" /> <i className="fab fa-instagram"></i>
</div> </a>
<p>Kediri Technopark adalah pusat pengembangan inovasi digital dan aplikasi untuk masyarakat dan pelaku usaha.</p> <a href="https://linkedin.com/company/kediri-technopark" target="_blank" rel="noopener noreferrer" className={styles.socialLink}>
<i className="fab fa-linkedin-in"></i>
</a>
<a href="https://facebook.com/kediritechnopark" target="_blank" rel="noopener noreferrer" className={styles.socialLink}>
<i className="fab fa-facebook-f"></i>
</a>
</div> </div>
</Col> </Col>
<Col lg={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}>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>&copy; 2025 Kediri Technopark. All Rights Reserved.</p> <p>&copy; 2025 Kediri Technopark. All Rights Reserved.</p>
</div> </div>
</Col> </Col>

View File

@@ -0,0 +1,252 @@
.footer {
background-color: #1e293b;
color: #e2e8f0;
padding: 50px 0 0;
position: relative;
overflow: hidden;
}
.footerContent {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.footerColumn {
margin-bottom: 25px;
}
.footerLogo {
margin-bottom: 15px;
}
.logoImage {
max-width: 160px;
height: auto;
}
.companyDescription {
color: #94a3b8;
line-height: 1.6;
margin-bottom: 20px;
font-size: 0.9rem;
}
.socialLinks {
display: flex;
gap: 15px;
}
.socialLink {
color: #ffffff;
transition: all 0.3s ease;
text-decoration: none;
font-size: 1.2rem;
}
.socialLink:hover {
color: #0057b8;
transform: translateY(-2px);
}
.footerTitle {
color: #f1f5f9;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 15px;
position: relative;
}
.contactInfo {
display: flex;
flex-direction: column;
gap: 12px;
}
.contactItem {
display: flex;
gap: 10px;
align-items: flex-start;
color: #94a3b8;
font-size: 0.9rem;
line-height: 1.5;
}
.contactItem i {
color: #ffffff;
margin-top: 3px;
font-size: 0.9rem;
min-width: 16px;
}
.contactItem a {
color: #e2e8f0;
text-decoration: none;
transition: color 0.3s ease;
}
.contactItem a:hover {
color: #0057b8;
text-decoration: underline;
}
.footerLinks {
list-style: none;
padding: 0;
margin: 0 0 25px;
display: flex;
flex-direction: column;
gap: 10px;
}
.footerLinks li a {
color: #94a3b8;
text-decoration: none;
transition: all 0.3s ease;
font-size: 0.9rem;
display: block;
padding: 2px 0;
}
.footerLinks li a:hover {
color: #0057b8;
transform: translateX(3px);
}
.newsletter p {
color: #94a3b8;
margin-bottom: 12px;
font-size: 0.9rem;
line-height: 1.5;
}
.newsletterForm {
display: flex;
flex-direction: column;
gap: 10px;
}
.newsletterInput {
padding: 10px 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.05);
color: #e2e8f0;
font-size: 0.85rem;
}
.newsletterInput::placeholder {
color: #94a3b8;
}
.newsletterInput:focus {
outline: none;
border-color: #0057b8;
background-color: rgba(255, 255, 255, 0.08);
}
.newsletterButton {
background: #0057b8;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.newsletterButton:hover {
background: #004a9e;
transform: translateY(-1px);
}
.footerBottom {
padding: 20px 0;
}
.copyright {
text-align: center;
}
.copyright p {
color: #94a3b8;
margin: 0;
font-size: 0.85rem;
}
/* Responsive design */
@media (max-width: 992px) {
.footer {
padding: 40px 0 0;
}
.footerContent {
margin-bottom: 35px;
}
.footerColumn {
flex: 0 0 50%;
max-width: 50%;
}
.logoImage {
max-width: 140px;
}
}
@media (max-width: 768px) {
.footer {
padding: 35px 0 0;
}
.footerContent {
margin-bottom: 30px;
}
.footerColumn {
flex: 0 0 100%;
max-width: 100%;
margin-bottom: 25px;
}
.footerTitle {
font-size: 1.05rem;
}
.newsletterForm {
flex-direction: row;
}
.newsletterInput {
flex: 1;
}
}
@media (max-width: 576px) {
.footer {
padding: 30px 0 0;
}
.footerContent {
margin-bottom: 25px;
}
.socialLinks {
gap: 12px;
}
.socialLink {
font-size: 1.1rem;
}
.newsletterForm {
flex-direction: column;
}
.newsletterInput,
.newsletterButton {
width: 100%;
}
}

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import styles from './Styles.module.css'; import styles from './Styles.module.css';
@@ -6,53 +6,108 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
const navigate = useNavigate(); const navigate = useNavigate();
const [hoveredNav, setHoveredNav] = useState(null); const [hoveredNav, setHoveredNav] = useState(null);
const [menuOpen, setMenuOpen] = useState(false); // toggle mobile menu const [menuOpen, setMenuOpen] = useState(false); // toggle mobile menu
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setIsScrolled(window.scrollY > 10);
window.addEventListener('scroll', onScroll);
onScroll();
return () => window.removeEventListener('scroll', onScroll);
}, []);
const scrollToId = (id) => {
// Ensure we are on home, then scroll to target id smoothly
navigate('/');
setTimeout(() => {
const el = document.getElementById(id);
if (el) el.scrollIntoView({ behavior: 'smooth' });
// Backward compatibility with refs passed from App for products/academy
if (id === 'products' && typeof scrollToProduct === 'function') scrollToProduct();
if (id === 'academy' && typeof scrollToCourse === 'function') scrollToCourse();
}, 0);
};
// Close mobile menu when window is resized to desktop size
useEffect(() => {
const handleResize = () => {
if (window.innerWidth > 600) {
setMenuOpen(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return ( return (
<header className={styles.header}> <header className={`${styles.header} ${isScrolled ? styles.headerScrolled : ''}`}>
<img src="./kediri-technopark-logo.png" className={styles.logo} alt="Logo" /> <img src="./kediri-technopark-logo.png" className={styles.logo} alt="Logo" />
{/* Desktop Navigation */} {/* Desktop Navigation */}
<nav className={styles.nav}> <nav className={styles.nav}>
<a
className={`${styles.navLink} ${hoveredNav === 2 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(2)}
onMouseLeave={() => setHoveredNav(null)}
onClick={() => navigate('/')}
>
HOME
</a>
{username && {username &&
<a
className={`${styles.navLink} ${hoveredNav === 3 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(3)}
onMouseLeave={() => setHoveredNav(null)}
onClick={() => {
navigate('/dashboard');
}}>
DASHBOARD
</a>
}
{!username &&
<> <>
<a
className={`${styles.navLink} ${hoveredNav === 2 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(2)}
onMouseLeave={() => setHoveredNav(null)}
onClick={() => { navigate('/'); setMenuOpen(false); }}
>
Home
</a>
<a <a
className={`${styles.navLink} ${hoveredNav === 3 ? styles.navLinkHover : ''}`} className={`${styles.navLink} ${hoveredNav === 3 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(3)} onMouseEnter={() => setHoveredNav(3)}
onMouseLeave={() => setHoveredNav(null)} onMouseLeave={() => setHoveredNav(null)}
onClick={() => { onClick={() => {
navigate('/products'); navigate('/dashboard');
}} setMenuOpen(false);
}}>
Dashboard
</a>
</>
}
{!username &&
<>
<a
className={`${styles.navLink} ${hoveredNav === 21 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(21)}
onMouseLeave={() => setHoveredNav(null)}
onClick={() => { scrollToId('about'); setMenuOpen(false); }}
> >
PRODUCTS About
</a>
<a
className={`${styles.navLink} ${hoveredNav === 22 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(22)}
onMouseLeave={() => setHoveredNav(null)}
onClick={() => { scrollToId('services'); setMenuOpen(false); }}
>
Services
</a>
<a
className={`${styles.navLink} ${hoveredNav === 3 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(3)}
onMouseLeave={() => setHoveredNav(null)}
onClick={() => { scrollToId('products'); setMenuOpen(false); }}
>
Products
</a> </a>
<a <a
className={`${styles.navLink} ${hoveredNav === 4 ? styles.navLinkHover : ''}`} className={`${styles.navLink} ${hoveredNav === 4 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(4)} onMouseEnter={() => setHoveredNav(4)}
onMouseLeave={() => setHoveredNav(null)} onMouseLeave={() => setHoveredNav(null)}
onClick={() => { onClick={() => { scrollToId('academy'); setMenuOpen(false); }}
scrollToCourse();
}}
> >
ACADEMY Academy
</a>
<a
className={`${styles.navLink} ${hoveredNav === 5 ? styles.navLinkHover : ''}`}
onMouseEnter={() => setHoveredNav(5)}
onMouseLeave={() => setHoveredNav(null)}
onClick={() => { scrollToId('faq'); setMenuOpen(false); }}
>
FAQ
</a> </a>
</> </>
} }
@@ -69,11 +124,17 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
{username ? ( {username ? (
<> <>
<div className={styles.username}>{username}</div> <div className={styles.username}>{username}</div>
<button onClick={() => { setMenuOpen(false); navigate('/'); }}>Home</button>
<button onClick={() => { setMenuOpen(false); scrollToId('about'); }}>About</button>
<button onClick={() => { setMenuOpen(false); scrollToId('services'); }}>Services</button>
<button onClick={() => { setMenuOpen(false); scrollToId('products'); }}>Products</button>
<button onClick={() => { setMenuOpen(false); scrollToId('academy'); }}>Academy</button>
<button onClick={() => { setMenuOpen(false); scrollToId('faq'); }}>FAQ</button>
<button className={styles.logoutButton} onClick={() => { <button className={styles.logoutButton} onClick={() => {
navigate('/dashboard'); navigate('/dashboard');
setMenuOpen(false);
}}> }}>
DASHBOARD Dashboard
</button> </button>
<button className={styles.logoutButton} onClick={() => { <button className={styles.logoutButton} onClick={() => {
@@ -84,15 +145,23 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
</button> </button>
</> </>
) : ( ) : (
<button <>
className={styles.loginButton} <button onClick={() => { setMenuOpen(false); navigate('/'); }}>Home</button>
onClick={() => { <button onClick={() => { setMenuOpen(false); scrollToId('about'); }}>About</button>
setMenuOpen(false); <button onClick={() => { setMenuOpen(false); scrollToId('services'); }}>Services</button>
setShowedModal('login'); <button onClick={() => { setMenuOpen(false); scrollToId('products'); }}>Products</button>
}} <button onClick={() => { setMenuOpen(false); scrollToId('academy'); }}>Academy</button>
> <button onClick={() => { setMenuOpen(false); scrollToId('faq'); }}>FAQ</button>
LOGIN <button
</button> className={styles.loginButton}
onClick={() => {
setMenuOpen(false);
setShowedModal('login');
}}
>
Sign in
</button>
</>
)} )}
</div> </div>
)} )}
@@ -107,7 +176,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
)} )}
{!username && ( {!username && (
<button className={styles.loginButton} onClick={() => setShowedModal('login')}> <button className={styles.loginButton} onClick={() => setShowedModal('login')}>
LOGIN Sign in
</button> </button>
)} )}
</div> </div>

View File

@@ -1,16 +1,22 @@
// HeroSection.jsx — 2025 refresh using React-Bootstrap + CSS Module // HeroSection.jsx — 2025 refresh using React-Bootstrap + CSS Module
import React from 'react'; import React from 'react';
import { Container, Row, Col, Button } from 'react-bootstrap'; import { Container, Row, Col, Button } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
import styles from './HeroSection.module.css'; import styles from './HeroSection.module.css';
const HeroSection = () => { const HeroSection = () => {
const navigate = useNavigate();
const goProducts = () => navigate('/products');
const goAcademy = () => navigate('/#services');
return ( return (
<section className={`${styles.hero} pt-5`} <section className={`${styles.hero} pt-3 pb-3`}
aria-label="Kediri Technopark hero section"> aria-label="Kediri Technopark hero section">
<Container> <Container className={styles.heroContainer}>
<Row className="align-items-center gy-5"> <Row className="align-items-center gy-3">
{/* Image on top for mobile, text first on lg+ */} {/* Text first for mobile and desktop for clarity */}
<Col xs={{ order: 1 }} lg={{ span: 6, order: 1 }}> <Col xs={{ order: 0 }} lg={{ span: 8, order: 1 }} xl={{ span: 7, order: 1 }}>
<div className={styles.copyWrap}> <div className={styles.copyWrap}>
<h1 className={styles.title}> <h1 className={styles.title}>
KATALIS KARIR DAN BISNIS DIGITAL KATALIS KARIR DAN BISNIS DIGITAL
@@ -18,18 +24,27 @@ const HeroSection = () => {
<p className={styles.lead}> <p className={styles.lead}>
Kami adalah ekosistem tempat mimpi digital tumbuh dan masa depan dibentuk. Di sinilah semangat belajar bertemu dengan inovasi, dan ide-ide muda diberi ruang untuk berkembang. Lebih dari sekadar tempat, kami adalah rumah bagi talenta, teknologi, dan transformasi. Mari jelajahi dunia digital, bangun karir, dan ciptakan solusi semua dimulai dari sini. Kami adalah ekosistem tempat mimpi digital tumbuh dan masa depan dibentuk. Di sinilah semangat belajar bertemu dengan inovasi, dan ide-ide muda diberi ruang untuk berkembang. Lebih dari sekadar tempat, kami adalah rumah bagi talenta, teknologi, dan transformasi. Mari jelajahi dunia digital, bangun karir, dan ciptakan solusi semua dimulai dari sini.
</p> </p>
<div className={styles.ctaGroup}>
<Button className={styles.ctaPrimary} onClick={goProducts}>
Explore Products
</Button>
<Button variant="light" className={styles.ctaSecondary} onClick={goAcademy}>
View Academy
</Button>
</div>
</div> </div>
</Col> </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}> <div className={styles.imageWrap}>
<img <div className={styles.imageFrame}>
src="https://kediritechnopark.com/assets/hero.png" <img
alt="Ekosistem digital Kediri Technopark" src="https://kediritechnopark.com/assets/hero.png"
className={`img-fluid ${styles.heroImage}`} alt="Ekosistem digital Kediri Technopark"
loading="lazy" className={`img-fluid ${styles.heroImage}`}
decoding="async" loading="lazy"
/> decoding="async"
<div className={styles.glow} aria-hidden="true" /> />
</div>
</div> </div>
</Col> </Col>
</Row> </Row>

View File

@@ -1,58 +1,141 @@
.hero { .hero {
position: relative; position: relative;
background: radial-gradient(1200px 600px at 10% -10%, rgba(37, 99, 235, 0.15), transparent 60%), background:
radial-gradient(1000px 500px at 110% 10%, rgba(34, 197, 94, 0.15), transparent 60%), radial-gradient(900px 400px at 0% -10%, color-mix(in srgb, var(--brand) 12%, transparent), transparent 60%),
var(--surface); radial-gradient(800px 350px at 110% 0%, rgba(0,0,0,0.05), transparent 60%);
overflow: clip; 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 { .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 { .title {
font-weight: 800; font-weight: 800;
line-height: 1.08; line-height: 1.15;
letter-spacing: -0.02em; letter-spacing: -0.02em;
/* Fluid type: min 28px → max 56px */ /* Fluid type: min 24px → max 40px, ensure 1-line on desktop */
font-size: clamp(1.75rem, 2.5vw + 1rem, 3.5rem); font-size: clamp(1.8rem, 2vw + 0.8rem, 2.8rem);
background: linear-gradient(92deg, var(--text), #3b82f6 40%, #22c55e 90%); background: linear-gradient(92deg, var(--text) 0%, color-mix(in srgb, var(--brand) 70%, #0f172a) 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
color: black; color: transparent;
white-space: normal;
} }
.lead { .lead {
margin-top: 1rem; margin-top: 1rem;
color: var(--muted); 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 { .ctaGroup {
margin-top: 1.25rem; margin-top: 0.75rem;
position: relative;
z-index: 2;
} }
.cta { .ctaPrimary {
--ring: 0 0 0 0 rgba(37,99,235,0); background: var(--brand) !important;
border-radius: var(--radius-2xl) !important; color: #fff !important;
padding: 0.625rem 1rem !important; border: 1px solid var(--brand) !important;
border-radius: 12px !important;
padding: 0.45rem 0.8rem !important;
font-weight: 600 !important; font-weight: 600 !important;
backdrop-filter: saturate(140%); letter-spacing: 0.02em;
transition: transform .2s ease, box-shadow .2s ease, background-color .2s ease; transition: background-color .16s ease, border-color .16s ease;
box-shadow: var(--shadow-soft); 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:hover,
.ctaPrimary:focus-visible { .ctaPrimary:focus-visible,
transform: translateY(-1px);
box-shadow: 0 12px 32px rgba(37, 99, 235, 0.25);
}
.ctaSecondary:hover, .ctaSecondary:hover,
.ctaSecondary:focus-visible { .ctaSecondary:focus-visible {
transform: translateY(-1px); box-shadow: var(--shadow-neutral-s);
box-shadow: 0 12px 32px rgba(34, 197, 94, 0.25); transform: none;
} }
.imageWrap { .imageWrap {
@@ -60,33 +143,32 @@
display: grid; display: grid;
place-items: center; place-items: center;
isolation: isolate; isolation: isolate;
width: 100%;
max-width: 650px;
margin-left: auto;
z-index: -1;
pointer-events: none;
} }
.imageWrap::before, .imageWrap::before, .imageWrap::after { content: none; }
.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 { .imageFrame {
left: 0; position: relative;
background: linear-gradient(to right, rgba(255,255,255,1), rgba(255,255,255,0)); 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 { .heroImage {
border-radius: var(--radius-2xl); display: block;
box-shadow: var(--shadow-soft); width: 100%;
height: auto;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: calc(var(--radius-2xl) - 4px);
} }
.glow { .glow {
@@ -95,15 +177,62 @@
height: 40%; height: 40%;
filter: blur(40px); filter: blur(40px);
z-index: -1; z-index: -1;
background: radial-gradient(60% 60% at 50% 0%, rgba(59,130,246,.35), transparent 60%), background: radial-gradient(60% 60% at 50% 0%, color-mix(in srgb, var(--brand) 30%, transparent), transparent 60%);
radial-gradient(50% 50% at 30% 60%, rgba(34,197,94,.25), 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 */ /* Fine-tuned responsive spacing */
@media (min-width: 992px) { @media (min-width: 992px) {
.copyWrap { padding-right: 1rem; } .copyWrap { padding-right: 1rem; }
} }
@media (max-width: 575.98px) { @media (max-width: 575.98px) {
.hero { padding-top: 2rem; } .hero { padding-top: 1rem; }
.ctaGroup { display: grid; gap: 8px; }
.ctaPrimary, .ctaSecondary { width: 100% !important; text-align: center; }
.copyWrap { max-width: 100%; padding: 0 10px; }
.title { font-size: clamp(1.3rem, 2.5vw + 1rem, 1.8rem); }
.lead { font-size: clamp(0.9rem, 0.5vw + 0.8rem, 1rem); }
} }
@media (max-width: 767.98px) {
.imageWrap::before,
.imageWrap::after { display: none; }
.title { font-size: clamp(1.4rem, 2vw + 1rem, 2.1rem); line-height: 1.12; }
.lead { font-size: clamp(0.93rem, 0.4vw + 0.84rem, 1.03rem); }
.bulletItem { font-size: 0.92rem; }
.mesh, .grid { display: none; }
.copyWrap { max-width: 100%; padding: 0 15px; }
.imageWrap { max-width: 100%; }
.imageFrame { border-radius: calc(var(--radius-2xl) + 2px); }
.heroImage { border-radius: calc(var(--radius-2xl) - 6px); }
}
.imageFrame:hover { box-shadow: none; transform: none; }
@media (min-width: 1400px) {
.imageWrap { max-width: 720px; }
}
@media (max-width: 400px) {
.hero { padding-top: 0.8rem; }
.title { font-size: clamp(1.2rem, 3vw + 0.9rem, 1.7rem); }
.lead { font-size: clamp(0.85rem, 0.5vw + 0.75rem, 0.95rem); }
.ctaGroup { gap: 6px; }
.ctaPrimary, .ctaSecondary {
padding: 0.4rem 0.7rem !important;
font-size: 0.9rem;
}
.copyWrap { padding: 0 5px; }
}

View File

@@ -0,0 +1,54 @@
import React from 'react';
import styles from './ProductCard.module.css';
const ProductCard = ({ product, onCardClick, isCenter, canHover, onCollapse }) => {
return (
<div
className={`${styles.card} ${isCenter ? styles.isCenter : ''} ${canHover ? styles.canHover : ''}`}
>
<img
src={product.image}
alt={product.name}
className={styles.cover}
onError={(e) => { e.currentTarget.src = '/assets/images/placeholder-product.png'; }}
/>
<div
className={styles.overlay}
onClick={(e) => {
// Collapse overlay when clicking on the overlay background (not buttons)
if (isCenter && canHover && onCollapse) {
// Check if the click target is the overlay itself, not a button
if (e.target === e.currentTarget) {
e.stopPropagation();
onCollapse();
}
}
}}
>
<div className={styles.overlayInner}>
<h3 className={styles.title}>{product.name}</h3>
<div className={styles.meta}>
<p className={styles.description}>{product.description}</p>
<div className={styles.buttonGroup}>
<button
className={styles.detailButton}
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onCardClick && onCardClick(product); }}
>
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;

View File

@@ -0,0 +1,171 @@
/* 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: 768px) {
.title { font-size: 0.9rem; }
.overlay { --overlay-collapsed: 60px; }
.description { font-size: 0.8rem; }
.buttonGroup { gap: 6px; }
.detailButton,
.buyButton {
padding: 5px 10px;
font-size: 0.75rem;
}
}
@media (max-width: 576px) {
.title { font-size: 0.85rem; }
.overlay { --overlay-collapsed: 50px; }
.overlayInner { padding: 10px 12px 12px; }
.description { font-size: 0.75rem; margin: 6px 0 8px; }
.buttonGroup { gap: 5px; }
.detailButton,
.buyButton {
padding: 4px 8px;
font-size: 0.7rem;
}
}
@media (max-width: 400px) {
.title { font-size: 0.8rem; }
.overlay { --overlay-collapsed: 45px; }
.overlayInner { padding: 8px 10px 10px; }
.description { font-size: 0.7rem; margin: 4px 0 6px; }
.buttonGroup { gap: 4px; }
.detailButton,
.buyButton {
padding: 3px 6px;
font-size: 0.65rem;
}
}

View File

@@ -1,16 +1,16 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Container } from 'react-bootstrap'; import { Container } from 'react-bootstrap';
import styles from './Styles.module.css'; import styles from './ProductSection.module.css';
import CoverflowCarousel from './CoverflowCarousel';
import processProducts from '../helper/processProducts'; import processProducts from '../helper/processProducts';
import shared from './Styles.module.css';
import useInView from '../hooks/useInView';
const ProductSection = ({ setSelectedProduct, setShowedModal, productSectionRef, setWillDo }) => { const ProductSection = ({ setSelectedProduct, setShowedModal, productSectionRef, setWillDo }) => {
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
const [hoveredCard, setHoveredCard] = useState(null); const [filteredProducts, setFilteredProducts] = useState([]);
// Define this function outside useEffect so it can be called anywhere const [selectedCategory, setSelectedCategory] = useState('all');
// Inside your component
useEffect(() => { useEffect(() => {
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', { fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST', method: 'POST',
@@ -21,70 +21,79 @@ const ProductSection = ({ setSelectedProduct, setShowedModal, productSectionRef,
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
const enrichedData = processProducts(data); const processed = processProducts(data);
setProducts(enrichedData); setProducts(processed);
setFilteredProducts(processed);
}) })
.catch(err => console.error('Fetch error:', err)); .catch(err => console.error('Fetch error:', err));
}, []); }, []);
return ( // Extract unique categories from products
const categories = ['all', ...new Set(products.map(product => product.category).filter(Boolean))];
<section id="services" className="services pt-5" 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" style={{scrollMarginTop: '65px' }} className={`${styles.productSection} ${shared.revealSection} ${inView ? shared.isVisible : ''}`} ref={(el) => {
if (typeof productSectionRef === 'function') productSectionRef(el);
if (ref) ref.current = el;
}}>
<Container> <Container>
<div className="section-heading mb-4"> <div className={styles.sectionHeader}>
<h4>OUR <em>PRODUCTS</em></h4> <h2 className={styles.sectionTitle}>Produk Unggulan</h2>
<img src="/assets/images/heading-line-dec.png" alt="" /> <p>Produk digital siap pakai untuk mempercepat pertumbuhan bisnis Anda.</p>
<p>Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.</p>
</div> </div>
<div className={styles.coursesGrid}>
{products && {/* Category Filter */}
products[0]?.name && {categories.length > 2 && (
products <div className={styles.filterContainer}>
.map(product => ( <div className={styles.filterWrapper}>
<div <button
key={product.id} className={`${styles.filterBtn} ${selectedCategory === 'all' ? styles.active : ''}`}
className={`${styles.courseCard} ${hoveredCard === product.id ? styles.courseCardHover : ''}`} onClick={() => setSelectedCategory('all')}
onClick={() => { >
setSelectedProduct(product); Semua Produk
setShowedModal('product'); </button>
}} {categories.filter(cat => cat !== 'all').map(category => (
onMouseEnter={() => setHoveredCard(product.id)} <button
onMouseLeave={() => setHoveredCard(null)} key={category}
className={`${styles.filterBtn} ${selectedCategory === category ? styles.active : ''}`}
onClick={() => setSelectedCategory(category)}
> >
<div> {category}
<div className={styles.courseImage} style={{ backgroundImage: `url(${product.image})` }}> </button>
{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>
))} ))}
</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> </div>
</Container> </Container>
</section> </section>

View File

@@ -0,0 +1,307 @@
/* 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;
}
.sectionHeader p {
max-width: 100%;
padding: 0 15px;
}
.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;
padding: 0 10px;
}
.sectionTitle {
font-size: clamp(1.4rem, 4vw, 1.8rem);
}
.sectionHeader p {
font-size: 0.95rem;
}
.filterContainer {
margin-bottom: 20px;
padding: 0 10px;
}
.filterWrapper {
gap: 6px;
}
.filterBtn {
padding: 5px 10px;
font-size: 0.75rem;
}
.carouselContainer {
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;
padding: 0 15px;
}
.sectionTitle {
font-size: clamp(1.3rem, 5vw, 1.7rem);
}
.sectionHeader p {
font-size: 0.9rem;
}
.filterContainer {
margin-bottom: 15px;
padding: 0 15px;
}
.filterWrapper {
gap: 5px;
}
.filterBtn {
padding: 4px 8px;
font-size: 0.7rem;
}
.carouselContainer {
padding: 0 30px;
min-height: 320px;
}
.noProducts {
height: 220px;
font-size: 0.85rem;
}
}
@media (max-width: 400px) {
.productSection {
padding: 25px 0;
}
.sectionHeader {
margin-bottom: 15px;
padding: 0 10px;
}
.sectionTitle {
font-size: clamp(1.2rem, 6vw, 1.6rem);
}
.sectionHeader p {
font-size: 0.85rem;
}
.filterContainer {
margin-bottom: 12px;
padding: 0 10px;
}
.filterWrapper {
gap: 4px;
}
.filterBtn {
padding: 3px 6px;
font-size: 0.65rem;
}
.carouselContainer {
padding: 0 25px;
min-height: 300px;
}
.noProducts {
height: 200px;
font-size: 0.8rem;
}
}

View File

@@ -1,44 +1,71 @@
import React from 'react'; import React from 'react';
import { Container, Row, Col, Card } from 'react-bootstrap'; import { Container, Row, Col } from 'react-bootstrap';
import styles from './ServicesSection.module.css';
import { Network, Wrench, Code } from 'lucide-react';
import shared from './Styles.module.css';
import useInView from '../hooks/useInView';
const services = [
{
icon: <Network size={28} />,
title: "Meshticon",
theme: "Network",
description: "Instalasi jaringan dan infrastruktur terstruktur yang memastikan konektivitas andal dan keamanan aset Anda, sehingga operasional bisnis berjalan mulus tanpa gangguan.",
},
{
icon: <Wrench size={28} />,
title: "Techcare",
theme: "Support",
description: "Menyediakan layanan perakitan, servis, dan konsultasi perangkat keras untuk menjamin kinerja optimal dan penanganan masalah yang responsif, sehingga produktivitas tim Anda terjaga dan investasi teknologi Anda lebih awet.",
},
{
icon: <Code size={28} />,
title: "Gawechno",
theme: "Software",
description: "Pengembangan software, website, dan sistem otomasi kustom yang mengubah proses manual menjadi lebih efisien dan cerdas, memungkinkan Anda menekan biaya operasional dan membuka jalan bagi inovasi bisnis.",
},
];
const ServicesSection = () => { const ServicesSection = () => {
const { ref, inView } = useInView();
return ( return (
<section id="services" className="services py-5"> <section id="services" ref={ref} style={{scrollMarginTop: '65px'}} className={`${styles.blueprintContainer} ${shared.revealSection} ${inView ? shared.isVisible : ''}`}>
<Container> <div className={styles.blueprintGrid}></div>
<div className="section-heading mb-4"> <div className={styles.contentWrapper}>
<h4>OUR <em>SERVICES</em></h4> <Container>
<img src="/assets/images/heading-line-dec.png" alt="" /> <div className={styles.sectionHeader}>
<p>Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.</p> {/* Judul diubah */}
</div> <h2 className={styles.sectionTitle}>Layanan Kami</h2>
<Row> <img src="/assets/images/heading-line-dec.png" alt="" className={styles.headingLine} />
<Col lg={4}> <p>
<Card className="mb-4"> Kami menyediakan berbagai solusi teknologi untuk mendukung transformasi digital bisnis dan masyarakat.
<Card.Body> </p>
<Card.Title>Mesthicon</Card.Title> </div>
<Card.Text>Layanan instalasi jaringan, CCTV, dan infrastruktur teknologi.</Card.Text> <Row className="gy-4 justify-content-center">
</Card.Body> {services.map((service, index) => (
</Card> <Col key={index} lg={4} md={6}>
</Col> <div className={styles.specCard}>
<Col lg={4}> {/* Stuktur header disederhanakan */}
<Card className="mb-4"> <div className={styles.cardHeader}>
<Card.Body> <div className={styles.cardIcon}>{service.icon}</div>
<Card.Title>Techcare</Card.Title> <h3 className={styles.cardTitle}>{service.title}</h3>
<Card.Text>Perakitan komputer, servis, dan konsultasi infrastruktur digital.</Card.Text> <div className={styles.themePill}>
</Card.Body> {/* Urutan dibalik: titik dulu, baru tulisan */}
</Card> <span className={styles.glowingDot}></span>
</Col> <span>{service.theme}</span>
<Col lg={4}> </div>
<Card className="mb-4"> </div>
<Card.Body> <div className={styles.cardBody}>
<Card.Title>Gawechno</Card.Title> <p className={styles.cardDescription}>{service.description}</p>
<Card.Text>Pembuatan software, website, sistem otomatisasi bisnis, dan aplikasi AI.</Card.Text> </div>
</Card.Body> </div>
</Card> </Col>
</Col> ))}
</Row> </Row>
</Container> </Container>
</div>
</section> </section>
); );
}; };
export default ServicesSection; export default ServicesSection;

View 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 */

View File

@@ -23,15 +23,28 @@
} }
.header { .header {
background-color: white; position: sticky;
top: 0;
z-index: 1000;
padding: 14px 6rem; padding: 14px 6rem;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); background: transparent; /* blend with top page */
/* position: sticky; */ -webkit-backdrop-filter: saturate(140%) blur(14px);
top: 0; backdrop-filter: saturate(140%) blur(14px);
z-index: 1000; border-bottom: none;
box-shadow: none;
}
.headerScrolled {
background: rgba(255, 255, 255, 0.65);
border-bottom: 1px solid rgba(203, 213, 225, 0.8);
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
}
@supports not ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) {
.header { /* revert: no backdrop effects */ }
} }
.logo { .logo {
@@ -83,12 +96,19 @@
} }
.loginButton { .loginButton {
background-color: transparent; background-color: #2563eb; /* brand color */
color: #64748b; color: #ffffff;
border: none; border: none;
font-size: 0.9rem; font-size: 0.9rem;
cursor: pointer; cursor: pointer;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 10px; /* rounded corner */
font-weight: 600;
transition: background-color 0.2s ease;
}
.loginButton:hover {
background-color: #1e40af;
} }
/* Hero Section */ /* Hero Section */
@@ -175,6 +195,53 @@
margin: 0 auto; margin: 0 auto;
} }
.sectionHeader {
margin-bottom: 1.5rem;
text-align: left;
}
.sectionEyebrow {
display: inline-block;
padding: 4px 10px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #1e40af;
background: #e8f0ff;
border: 1px solid #c7d2fe;
border-radius: 999px;
margin-bottom: 8px;
}
.sectionTitle {
margin: 4px 0 6px 0;
color: #0f172a;
font-weight: 800;
font-size: 2rem;
line-height: 1.2;
}
@media (max-width: 600px) {
.sectionTitle { font-size: 1.5rem; }
}
.sectionRule {
width: 72px;
height: 4px;
background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%);
border-radius: 999px;
margin: 8px 0 12px 0;
}
.sectionSubtitle {
color: #475569;
font-size: 0.98rem;
line-height: 1.6;
max-width: 900px;
margin: 0;
}
.coursesTitle { .coursesTitle {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: bold; font-weight: bold;
@@ -184,15 +251,23 @@
.coursesGrid { .coursesGrid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); grid-template-columns: 1fr;
gap: 2rem; gap: 1.25rem;
}
@media (min-width: 640px) {
.coursesGrid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 992px) {
.coursesGrid { grid-template-columns: repeat(3, 1fr); }
} }
.courseCard { .courseCard {
background-color: white; background-color: white;
border-radius: 1rem; border-radius: 12px;
overflow: hidden; overflow: hidden;
box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.3); border: 1px solid #e5e7eb; /* thin border, no base shadow */
transition: all 0.3s ease; transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
cursor: pointer; cursor: pointer;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
-webkit-user-select: none; -webkit-user-select: none;
@@ -210,13 +285,14 @@
.courseCardHover { .courseCardHover {
transform: translateY(-4px); transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); border-color: #d1d5db;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
} }
.courseImage { .courseImage {
width: 100%; width: 100%;
height: 200px; height: 160px;
background-color: white; background-color: white;
position: relative; position: relative;
display: flex; display: flex;
@@ -225,7 +301,7 @@
color: #64748b; color: #64748b;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: cover;
background-position: center; background-position: center;
} }
@@ -242,10 +318,17 @@
} }
.courseContentTop { .courseContentTop {
padding: 1rem 1rem 0rem; padding: 0.75rem 0.75rem 0rem;
text-align: left; text-align: left;
} }
.titleRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.courseContentTop p { .courseContentTop p {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -253,11 +336,12 @@
.courseContentBottom { .courseContentBottom {
padding: 1rem; padding: 0.75rem;
padding-top: 0; padding-top: 0.5rem;
text-align: left; text-align: left;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
} }
.courseCategory { .courseCategory {
@@ -269,26 +353,59 @@
} }
.courseTitle { .courseTitle {
font-size: 1.2rem; flex: 1 1 auto;
font-weight: bold; min-width: 0; /* allow ellipsis shrink in flex */
color: #1e293b; font-size: 1rem;
margin-bottom: 1rem; font-weight: 700;
margin-top: 0; color: #0f172a;
margin: 0; /* in-row with pills */
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.courseStats { .pillRow {
display: flex; display: flex;
justify-content: space-between; gap: 0.4rem;
flex-wrap: wrap;
align-items: center; align-items: center;
margin-bottom: 1rem; justify-content: flex-end;
font-size: 0.9rem; flex: 0 0 auto;
color: #64748b; margin-left: 0.5rem;
} }
.courseStat { .pill {
display: flex; padding: 2px 8px;
align-items: center; background-color: #f3f4f6;
gap: 0.25rem; color: #374151;
border: 1px solid #e5e7eb;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
line-height: 1.4;
}
@media (max-width: 600px) {
.courseTitle { font-size: 0.95rem; }
.pill { font-size: 10px; }
}
.pillModules {
background-color: #e8f0ff;
border-color: #c7d2fe;
color: #1e40af;
}
.pillSessions {
background-color: #ecfdf5;
border-color: #bbf7d0;
color: #065f46;
}
.titleSeparator {
border-top: 1px dashed #e5e7eb;
margin: 0.5rem 0 0.5rem;
} }
.coursePrice { .coursePrice {
@@ -310,11 +427,43 @@
} }
.freePrice { .freePrice {
font-size: 1.2rem; font-size: 1.05rem;
font-weight: bold; font-weight: bold;
color: #059669; color: #059669;
} }
/* Compact description */
.courseDesc {
margin: 0.25rem 0 0.25rem;
color: #475569;
font-size: 0.9rem;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.enrollButton {
background-color: #2563eb;
color: #ffffff;
border: none;
padding: 0.55rem 0.9rem;
border-radius: 10px;
font-size: 0.95rem;
font-weight: 700;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.05s ease;
}
.enrollButton:hover {
background-color: #1e40af;
}
.enrollButton:active {
transform: translateY(1px);
}
.featuresContainer { .featuresContainer {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
@@ -343,6 +492,28 @@
gap: 2rem; gap: 2rem;
} }
/* Spacing for Academy section to avoid tight gap with FAQ */
.academySection {
padding-bottom: 3rem;
}
@media (max-width: 600px) {
.academySection { padding-bottom: 2rem; }
}
/* Reveal on scroll/load */
.revealSection {
opacity: 0;
transform: translateY(16px);
transition: opacity 480ms ease-out, transform 480ms ease-out;
will-change: opacity, transform;
}
.isVisible {
opacity: 1;
transform: translateY(0);
}
.featureItem { .featureItem {
display: flex; display: flex;
gap: 1.5rem; gap: 1.5rem;
@@ -490,16 +661,19 @@
/* Responsive Styles */ /* Responsive Styles */
@media (max-width: 800px) { @media (max-width: 800px) {
.modalBody { .modalBody {
width: 80%; width: 85%;
max-width: 90%;
margin: 0 15px;
} }
.header { .header {
padding: 14px 2rem; padding: 14px 1.5rem;
} }
.heroContainer { .heroContainer {
grid-template-columns: 1fr; grid-template-columns: 1fr;
text-align: center; text-align: center;
gap: 2rem;
} }
.ctaTitle, .ctaTitle,
@@ -514,7 +688,7 @@
.ctaCard, .ctaCard,
.Section { .Section {
padding: 2rem 0.8rem; padding: 2rem 1rem;
background-color: #f8fafc; background-color: #f8fafc;
} }
@@ -524,7 +698,7 @@
.ctaContainer, .ctaContainer,
.coursesGrid { .coursesGrid {
grid-template-columns: repeat(auto-fit, minmax(173px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.8rem; gap: 0.8rem;
} }
@@ -558,6 +732,7 @@
display: none; display: none;
font-size: 28px; font-size: 28px;
cursor: pointer; cursor: pointer;
user-select: none;
} }
.mobileMenu { .mobileMenu {
@@ -565,7 +740,7 @@
} }
/* Tampilkan burger dan menu di mobile */ /* Tampilkan burger dan menu di mobile */
@media (max-width: 600px) { @media (max-width: 768px) {
.nav { .nav {
display: none; display: none;
} }
@@ -586,11 +761,12 @@
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 8px; border-radius: 8px;
padding: 12px; padding: 12px;
z-index: 10; z-index: 1000;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-width: 150px;
} }
.mobileMenu button { .mobileMenu button {
@@ -600,6 +776,8 @@
color: white; color: white;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-size: 0.9rem;
text-align: left;
} }
.mobileMenu button:hover { .mobileMenu button:hover {
@@ -611,6 +789,108 @@
color: #2563eb; color: #2563eb;
margin-bottom: 4px; margin-bottom: 4px;
} }
.logoutButton {
background-color: transparent;
border: 1px solid #2563eb;
color: #2563eb;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
font-size: 0.9rem;
text-align: left;
}
.logoutButton:hover {
background-color: #2563eb;
color: white;
}
}
@media (max-width: 576px) {
.header {
padding: 12px 1rem;
}
.Section {
padding: 1.5rem 0.8rem;
}
.coursesGrid {
grid-template-columns: 1fr;
gap: 1rem;
}
.ctaContainer {
grid-template-columns: 1fr;
gap: 1rem;
}
.featuresList {
gap: 1.5rem;
}
.featureItem {
gap: 1rem;
}
.featureIcon {
width: 50px;
height: 50px;
font-size: 1.2rem;
}
.featureTitle {
font-size: 1.1rem;
}
.mobileMenu {
right: 15px;
top: 55px;
min-width: 140px;
}
.mobileMenu button {
padding: 6px 12px;
font-size: 0.85rem;
}
}
@media (max-width: 400px) {
.Section {
padding: 1.2rem 0.6rem;
}
.featureItem {
gap: 0.8rem;
}
.featureIcon {
width: 45px;
height: 45px;
font-size: 1rem;
}
.featureTitle {
font-size: 1rem;
}
.featureDescription {
font-size: 0.9rem;
}
.mobileMenu {
right: 10px;
top: 50px;
min-width: 130px;
padding: 10px;
}
.mobileMenu button {
padding: 5px 10px;
font-size: 0.8rem;
}
} }
.loggedInContainer { .loggedInContainer {

View 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
View 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 };
}

View File

@@ -1,3 +1,27 @@
:root {
--brand: #0057b8ff; /* primary brand color */
--brand-600: #004a9e; /* hover */
--brand-700: #003e85; /* active */
/* Hero and surface tokens */
--surface: #ffffff;
--text: #0f172a; /* slate-900 */
--muted: #475569; /* slate-600 */
--radius-2xl: 20px;
--shadow-soft: 0 20px 50px rgba(15, 23, 42, 0.08);
--header-h: 84px; /* approximate header height for hero sizing */
/* Neutral shadows for CTAs */
--shadow-neutral-s: 0 6px 16px rgba(17, 24, 39, 0.08);
--shadow-neutral-m: 0 8px 22px rgba(17, 24, 39, 0.12);
}
@media (max-width: 800px) {
:root { --header-h: 76px; }
}
@media (max-width: 600px) {
:root { --header-h: 70px; }
}
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',