Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@testing-library/user-event": "^13.5.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.5.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
Expand Down
Binary file added src/assets/GalleryNail1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/GalleryNail2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/GalleryNail3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/GalleryNail4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/HeroBanner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
209 changes: 209 additions & 0 deletions src/components/Gallery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import React, { useState, useRef, useEffect } from 'react';
import { FaPlay } from 'react-icons/fa';
import GalleryNail1 from '../assets/GalleryNail1.jpg';
import GalleryNail2 from '../assets/GalleryNail2.jpg';
import GalleryNail3 from '../assets/GalleryNail3.jpg';
import GalleryNail4 from '../assets/GalleryNail4.jpg';

const IMAGES = [GalleryNail1, GalleryNail2, GalleryNail3, GalleryNail4];
const IMAGE_WIDTH = 170; // Width of each image + margin
const CONTAINER_WIDTH = 30 * 16; // 30em to px (1em = 16px)

const Gallery = () => {
// Store position instead of rearranging images
const [position, setPosition] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
const sliderRef = useRef(null);

// Preload images
useEffect(() => {
IMAGES.forEach(src => {
const img = new Image();
img.src = src;
});

if (sliderRef.current?.parentElement) {
sliderRef.current.parentElement.style.width = `${CONTAINER_WIDTH}px`;
}
}, []);

// Create "circular" array with doubled images
const displayImages = [...IMAGES, ...IMAGES];

const rotateImagesRight = () => {
if (isAnimating) return;
setIsAnimating(true);

const slider = sliderRef.current;
if (!slider) return;

// Get current position and calculate next position
const nextPosition = (position + 1) % IMAGES.length;

// Set up smooth transition
slider.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
slider.style.transform = `translateX(-${(position + 1) * IMAGE_WIDTH}px)`;

// When transition completes
const handleTransitionEnd = () => {
// Check if we need to loop back
if (nextPosition === 0) {
// Immediately jump back to the first set without transition
slider.style.transition = 'none';
slider.style.transform = 'translateX(0)';
setPosition(0);
} else {
// Just update position
setPosition(nextPosition);
}

// Allow more animations after a brief delay
setTimeout(() => {
setIsAnimating(false);
}, 50);
};

slider.addEventListener('transitionend', handleTransitionEnd, { once: true });
};

const rotateImagesLeft = () => {
if (isAnimating) return;
setIsAnimating(true);

const slider = sliderRef.current;
if (!slider) return;

let newPosition = position - 1;

if (newPosition < 0) {
// Jump instantly to duplicate set (at end)
newPosition = IMAGES.length - 1;
slider.style.transition = 'none';
slider.style.transform = `translateX(-${IMAGES.length * IMAGE_WIDTH}px)`;

// Wait one animation frame, then animate back one
requestAnimationFrame(() => {
slider.style.transition = 'transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)';
slider.style.transform = `translateX(-${newPosition * IMAGE_WIDTH}px)`;
});

// After the real transition ends, update position
const handleTransitionEnd = () => {
setPosition(newPosition);
setTimeout(() => setIsAnimating(false), 50);
};
slider.addEventListener('transitionend', handleTransitionEnd, { once: true });
} else {
// Normal left movement
slider.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
slider.style.transform = `translateX(-${newPosition * IMAGE_WIDTH}px)`;

const handleTransitionEnd = () => {
setPosition(newPosition);
setTimeout(() => setIsAnimating(false), 50);
};
slider.addEventListener('transitionend', handleTransitionEnd, { once: true });
}
};

// Common styles
const styles = {
container: {
backgroundColor: '#FEE8ED',
padding: '2rem',
minHeight: '14em'
},
sliderContainer: {
overflow: 'hidden',
position: 'relative',
isolation: 'isolate'
},
button: {
position: 'absolute',
top: '50%',
right: '0.5em',
transform: 'translateY(-50%)',
backgroundColor: '#7d1260',
border: 'none',
color: '#FEE8ED',
width: '2.5em',
height: '2.5em',
borderRadius: '50%',
fontSize: '1rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
zIndex: 10
},
slider: {
display: 'flex',
transform: `translateX(-${position * IMAGE_WIDTH}px)`,
willChange: 'transform',
width: `${displayImages.length * IMAGE_WIDTH}px`,
backfaceVisibility: 'hidden',
perspective: '1000px'
},
imageContainer: {
width: `${IMAGE_WIDTH}px`,
flexShrink: 0,
contain: 'content',
transform: 'translateZ(0)'
},
image: {
height: '10em',
width: '10em',
borderRadius: '15px',
objectFit: 'cover',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
backfaceVisibility: 'hidden',
transform: 'translateZ(0)',
loading: 'eager'
}
};

return (
<div style={styles.container}>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div style={styles.sliderContainer}>
<button
onClick={rotateImagesLeft}
style={{
...styles.button,
left: '0.5em',
right: 'auto',
transform: 'translateY(-50%) rotate(180deg)'
}}
aria-label="Rotate gallery left"
disabled={isAnimating}
>
<FaPlay style={{ fontSize: '0.8em' }} />
</button>

<button
onClick={rotateImagesRight}
style={styles.button}
aria-label="Rotate gallery"
disabled={isAnimating}
>
<FaPlay style={{ fontSize: '0.8em' }} />
</button>

<div ref={sliderRef} style={styles.slider}>
{displayImages.map((img, idx) => (
<div
key={`${idx}-${img}`}
style={styles.imageContainer}
>
<img src={img} alt={`Nail design ${(idx % IMAGES.length) + 1}`} style={styles.image} />
</div>
))}
</div>
</div>
</div>
</div>
);
};

export default Gallery;
2 changes: 1 addition & 1 deletion src/components/HeroBanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function HeroBanner() {
backgroundImage: `url(${bannerImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
height: '100vh',
height: '75vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
Expand Down
4 changes: 2 additions & 2 deletions src/pages/TestPage.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

import Gallery from "../components/Gallery"

// add your components to the test page to see what they look like
function TestPage() {

return (
<>
<p>remove this if you want</p>
<Gallery></Gallery>
</>
);
}
Expand Down