diff --git a/public/index.html b/public/index.html index 446b368..6d0e252 100644 --- a/public/index.html +++ b/public/index.html @@ -3,11 +3,11 @@ - + @@ -26,7 +26,7 @@ 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`. --> - React App + Kediri Technopark diff --git a/src/components/AnimatedBackground.js b/src/components/AnimatedBackground.js index 357249c..76e263c 100644 --- a/src/components/AnimatedBackground.js +++ b/src/components/AnimatedBackground.js @@ -9,10 +9,27 @@ const AnimatedBackground = () => { const ctx = canvas.getContext('2d'); let animationFrameId; let particles = []; - const particleCount = 70; + + // 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; this.radius = 1.5; + 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 = () => { @@ -31,6 +48,9 @@ const AnimatedBackground = () => { 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)); @@ -40,7 +60,14 @@ const AnimatedBackground = () => { function connectParticles() { const cssWidth = canvas.clientWidth; const cssHeight = canvas.clientHeight; - const connectionDistance = 90; + + // 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++) { @@ -83,6 +110,14 @@ const AnimatedBackground = () => { 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; @@ -91,8 +126,8 @@ const AnimatedBackground = () => { if (p.y > cssHeight + p.radius) p.y = -p.radius; else if (p.y < -p.radius) p.y = cssHeight + p.radius; - p.x += p.vx; - p.y += p.vy; + p.x += p.vx * speedFactor; + p.y += p.vy * speedFactor; ctx.beginPath(); ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); @@ -106,11 +141,17 @@ const AnimatedBackground = () => { setupCanvas(); animate(); - window.addEventListener('resize', setupCanvas); + + // Handle resize events to adjust particle count + const handleResize = () => { + setupCanvas(); + }; + + window.addEventListener('resize', handleResize); return () => { cancelAnimationFrame(animationFrameId); - window.removeEventListener('resize', setupCanvas); + window.removeEventListener('resize', handleResize); }; }, []); diff --git a/src/components/CoverflowCarousel.js b/src/components/CoverflowCarousel.js index 8e3598c..ee02ba9 100644 --- a/src/components/CoverflowCarousel.js +++ b/src/components/CoverflowCarousel.js @@ -71,6 +71,15 @@ const CoverflowCarousel = ({ products, onCardClick }) => { 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; @@ -159,7 +168,15 @@ const CoverflowCarousel = ({ products, onCardClick }) => { animationState === 'initial' ? styles.initial : animationState === 'spread' ? styles.spread : '' }`} - onClick={() => { goToProduct(productIndex, position > 0 ? 'right' : (position < 0 ? 'left' : null)); }} + 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)); + } + }} >
@@ -168,6 +185,7 @@ const CoverflowCarousel = ({ products, onCardClick }) => { onCardClick={(p) => { onCardClick && onCardClick(p); }} isCenter={position === 0} canHover={position === 0 && animationState === 'ready' && !shiftDirection && !isDragging} + onCollapse={position === 0 ? collapseOverlay : undefined} />
diff --git a/src/components/CoverflowCarousel.module.css b/src/components/CoverflowCarousel.module.css index e7b8c93..4e798ab 100644 --- a/src/components/CoverflowCarousel.module.css +++ b/src/components/CoverflowCarousel.module.css @@ -10,6 +10,27 @@ 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; } @@ -415,6 +436,11 @@ .dotsContainer { bottom: -35px; } + + .leftMask, + .rightMask { + width: 80px; + } } @media (max-width: 992px) { @@ -453,6 +479,11 @@ width: 10px; height: 10px; } + + .leftMask, + .rightMask { + width: 70px; + } } @media (max-width: 768px) { @@ -497,21 +528,26 @@ width: 9px; height: 9px; } + + .leftMask, + .rightMask { + width: 60px; + } } @media (max-width: 576px) { .container { - height: 350px; + height: 320px; margin: 20px 0 40px 0; } .cardContainer { - width: 220px; - height: 270px; + width: 200px; + height: 250px; } .cardWrapper { - padding: 0 8px; + padding: 0 6px; } .navButton { @@ -541,17 +577,39 @@ 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: 320px; + height: 280px; margin: 15px 0 35px 0; } .cardContainer { - width: 200px; - height: 250px; + width: 180px; + height: 230px; } .navButton { @@ -572,4 +630,26 @@ 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; + } } diff --git a/src/components/Header.js b/src/components/Header.js index 6cfbcc9..24e8c47 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -27,6 +27,18 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han }, 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 (
Logo @@ -39,7 +51,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han className={`${styles.navLink} ${hoveredNav === 2 ? styles.navLinkHover : ''}`} onMouseEnter={() => setHoveredNav(2)} onMouseLeave={() => setHoveredNav(null)} - onClick={() => navigate('/')} + onClick={() => { navigate('/'); setMenuOpen(false); }} > Home @@ -49,6 +61,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han onMouseLeave={() => setHoveredNav(null)} onClick={() => { navigate('/dashboard'); + setMenuOpen(false); }}> Dashboard @@ -60,7 +73,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han className={`${styles.navLink} ${hoveredNav === 21 ? styles.navLinkHover : ''}`} onMouseEnter={() => setHoveredNav(21)} onMouseLeave={() => setHoveredNav(null)} - onClick={() => scrollToId('about')} + onClick={() => { scrollToId('about'); setMenuOpen(false); }} > About @@ -68,7 +81,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han className={`${styles.navLink} ${hoveredNav === 22 ? styles.navLinkHover : ''}`} onMouseEnter={() => setHoveredNav(22)} onMouseLeave={() => setHoveredNav(null)} - onClick={() => scrollToId('services')} + onClick={() => { scrollToId('services'); setMenuOpen(false); }} > Services @@ -76,7 +89,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han className={`${styles.navLink} ${hoveredNav === 3 ? styles.navLinkHover : ''}`} onMouseEnter={() => setHoveredNav(3)} onMouseLeave={() => setHoveredNav(null)} - onClick={() => scrollToId('products')} + onClick={() => { scrollToId('products'); setMenuOpen(false); }} > Products @@ -84,7 +97,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han className={`${styles.navLink} ${hoveredNav === 4 ? styles.navLinkHover : ''}`} onMouseEnter={() => setHoveredNav(4)} onMouseLeave={() => setHoveredNav(null)} - onClick={() => scrollToId('academy')} + onClick={() => { scrollToId('academy'); setMenuOpen(false); }} > Academy @@ -92,7 +105,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han className={`${styles.navLink} ${hoveredNav === 5 ? styles.navLinkHover : ''}`} onMouseEnter={() => setHoveredNav(5)} onMouseLeave={() => setHoveredNav(null)} - onClick={() => scrollToId('faq')} + onClick={() => { scrollToId('faq'); setMenuOpen(false); }} > FAQ @@ -111,7 +124,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han {username ? ( <>
{username}
- {/* */} + @@ -119,6 +132,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han diff --git a/src/components/HeroSection.module.css b/src/components/HeroSection.module.css index f393dd0..34d87b0 100644 --- a/src/components/HeroSection.module.css +++ b/src/components/HeroSection.module.css @@ -198,22 +198,41 @@ } @media (max-width: 575.98px) { - .hero { padding-top: 1.25rem; } + .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, 1.8vw + 1rem, 2.1rem); line-height: 1.12; } + .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; } } \ No newline at end of file diff --git a/src/components/ProductCard.js b/src/components/ProductCard.js index 8a8afdf..5ce60b2 100644 --- a/src/components/ProductCard.js +++ b/src/components/ProductCard.js @@ -1,7 +1,7 @@ import React from 'react'; import styles from './ProductCard.module.css'; -const ProductCard = ({ product, onCardClick, isCenter, canHover }) => { +const ProductCard = ({ product, onCardClick, isCenter, canHover, onCollapse }) => { return (
{
{ - if (isCenter) { - // Clicks on overlay open detail; prevent parent selection - e.stopPropagation(); - onCardClick && onCardClick(product); + // 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(); + } } }} > diff --git a/src/components/ProductCard.module.css b/src/components/ProductCard.module.css index 6ad8e02..d4150a5 100644 --- a/src/components/ProductCard.module.css +++ b/src/components/ProductCard.module.css @@ -132,7 +132,40 @@ .description { -webkit-line-clamp: 2; } } -@media (max-width: 576px) { +@media (max-width: 768px) { .title { font-size: 0.9rem; } - .overlay { --overlay-collapsed: 56px; } + .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; + } } diff --git a/src/components/ProductSection.module.css b/src/components/ProductSection.module.css index 3288154..7a45caa 100644 --- a/src/components/ProductSection.module.css +++ b/src/components/ProductSection.module.css @@ -152,6 +152,11 @@ margin-bottom: 25px; } + .sectionHeader p { + max-width: 100%; + padding: 0 15px; + } + .filterWrapper { gap: 8px; } @@ -179,6 +184,11 @@ .sectionHeader { margin-bottom: 25px; + padding: 0 10px; + } + + .sectionTitle { + font-size: clamp(1.4rem, 4vw, 1.8rem); } .sectionHeader p { @@ -187,6 +197,7 @@ .filterContainer { margin-bottom: 20px; + padding: 0 10px; } .filterWrapper { @@ -216,14 +227,33 @@ .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 35px; + padding: 0 30px; min-height: 320px; } @@ -234,8 +264,39 @@ } @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 30px; + padding: 0 25px; min-height: 300px; } diff --git a/src/components/Styles.module.css b/src/components/Styles.module.css index 0d31f27..fa850d3 100644 --- a/src/components/Styles.module.css +++ b/src/components/Styles.module.css @@ -661,16 +661,19 @@ /* Responsive Styles */ @media (max-width: 800px) { .modalBody { - width: 80%; + width: 85%; + max-width: 90%; + margin: 0 15px; } .header { - padding: 14px 2rem; + padding: 14px 1.5rem; } .heroContainer { grid-template-columns: 1fr; text-align: center; + gap: 2rem; } .ctaTitle, @@ -685,7 +688,7 @@ .ctaCard, .Section { - padding: 2rem 0.8rem; + padding: 2rem 1rem; background-color: #f8fafc; } @@ -695,7 +698,7 @@ .ctaContainer, .coursesGrid { - grid-template-columns: repeat(auto-fit, minmax(173px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.8rem; } @@ -729,6 +732,7 @@ display: none; font-size: 28px; cursor: pointer; + user-select: none; } .mobileMenu { @@ -736,7 +740,7 @@ } /* Tampilkan burger dan menu di mobile */ -@media (max-width: 600px) { +@media (max-width: 768px) { .nav { display: none; } @@ -757,11 +761,12 @@ border: 1px solid #ddd; border-radius: 8px; padding: 12px; - z-index: 10; + z-index: 1000; display: flex; flex-direction: column; gap: 10px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + min-width: 150px; } .mobileMenu button { @@ -771,6 +776,8 @@ color: white; border-radius: 6px; cursor: pointer; + font-size: 0.9rem; + text-align: left; } .mobileMenu button:hover { @@ -782,6 +789,108 @@ color: #2563eb; 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 {