update mobile

This commit is contained in:
karyamanswasta
2025-08-17 23:53:18 +07:00
parent cd5fb36279
commit 8d677eda1d
10 changed files with 420 additions and 42 deletions

View File

@@ -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);
};
}, []);

View File

@@ -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));
}
}}
>
<div className={styles.cardShadow} aria-hidden="true"></div>
<div className={styles.cardWrapper}>
@@ -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}
/>
</div>
</div>

View File

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

View File

@@ -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 (
<header className={`${styles.header} ${isScrolled ? styles.headerScrolled : ''}`}>
<img src="./kediri-technopark-logo.png" className={styles.logo} alt="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
</a>
@@ -49,6 +61,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
onMouseLeave={() => setHoveredNav(null)}
onClick={() => {
navigate('/dashboard');
setMenuOpen(false);
}}>
Dashboard
</a>
@@ -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
</a>
@@ -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
</a>
@@ -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
</a>
@@ -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
</a>
@@ -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
</a>
@@ -111,7 +124,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
{username ? (
<>
<div className={styles.username}>{username}</div>
{/* <button onClick={() => { setMenuOpen(false); navigate('/'); }}>Home</button> */}
<button onClick={() => { setMenuOpen(false); 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>
@@ -119,6 +132,7 @@ const Header = ({ username, scrollToProduct, scrollToCourse, setShowedModal, han
<button onClick={() => { setMenuOpen(false); scrollToId('faq'); }}>FAQ</button>
<button className={styles.logoutButton} onClick={() => {
navigate('/dashboard');
setMenuOpen(false);
}}>
Dashboard
</button>

View File

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

View File

@@ -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 (
<div
className={`${styles.card} ${isCenter ? styles.isCenter : ''} ${canHover ? styles.canHover : ''}`}
@@ -16,10 +16,13 @@ const ProductCard = ({ product, onCardClick, isCenter, canHover }) => {
<div
className={styles.overlay}
onClick={(e) => {
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();
}
}
}}
>

View File

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

View File

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

View File

@@ -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 {