|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>PixelFlow Carousel</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://unpkg.com/react@18/umd/react.development.js"></script> |
|
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> |
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
|
|
<style> |
|
|
.zoom-effect { |
|
|
transition: transform 0.3s ease; |
|
|
} |
|
|
.zoom-effect:hover { |
|
|
transform: scale(1.03); |
|
|
} |
|
|
.skeleton { |
|
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); |
|
|
background-size: 200% 100%; |
|
|
animation: shimmer 1.5s infinite; |
|
|
} |
|
|
@keyframes shimmer { |
|
|
0% { background-position: 200% 0; } |
|
|
100% { background-position: -200% 0; } |
|
|
} |
|
|
.dark .skeleton { |
|
|
background: linear-gradient(90deg, #2d3748 25%, #4a5568 50%, #2d3748 75%); |
|
|
} |
|
|
.carousel-transition { |
|
|
transition: opacity 0.5s ease, transform 0.5s ease; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-white dark:bg-gray-900 transition-colors duration-300"> |
|
|
<div id="root"></div> |
|
|
|
|
|
<script type="text/babel"> |
|
|
const { useState, useEffect, useRef } = React; |
|
|
|
|
|
const ImageCarousel = () => { |
|
|
const [images, setImages] = useState([]); |
|
|
const [currentIndex, setCurrentIndex] = useState(0); |
|
|
const [isLoading, setIsLoading] = useState(true); |
|
|
const [error, setError] = useState(null); |
|
|
const [isAutoPlaying, setIsAutoPlaying] = useState(true); |
|
|
const [apiSource, setApiSource] = useState('unsplash'); |
|
|
const [darkMode, setDarkMode] = useState(false); |
|
|
const timerRef = useRef(null); |
|
|
const carouselRef = useRef(null); |
|
|
|
|
|
const API_CONFIG = { |
|
|
unsplash: { |
|
|
url: '', |
|
|
params: {}, |
|
|
transform: () => [] |
|
|
}, |
|
|
pexels: { |
|
|
url: '', |
|
|
params: {}, |
|
|
transform: () => [] |
|
|
}, |
|
|
pixabay: { |
|
|
url: '', |
|
|
params: {}, |
|
|
transform: () => [] |
|
|
} |
|
|
}; |
|
|
|
|
|
const fetchImages = async () => { |
|
|
setIsLoading(true); |
|
|
setError(null); |
|
|
|
|
|
try { |
|
|
const config = API_CONFIG[apiSource]; |
|
|
|
|
|
setImages(Array(5).fill().map((_, i) => ({ |
|
|
url: `http://static.photos/nature/1024x576/${i+1}`, |
|
|
alt: `Placeholder Image ${i+1}`, |
|
|
author: 'Placeholder Author', |
|
|
authorUrl: '#' |
|
|
}))); |
|
|
return; |
|
|
const data = await response.json(); |
|
|
|
|
|
if (!response.ok) throw new Error(data.message || 'Failed to fetch images'); |
|
|
|
|
|
setImages(config.transform(data)); |
|
|
} catch (err) { |
|
|
console.error('Error fetching images:', err); |
|
|
setError(err.message || 'Failed to load images. Please try again.'); |
|
|
|
|
|
setImages(Array(5).fill().map((_, i) => ({ |
|
|
url: `http://static.photos/nature/1024x576/${i+1}`, |
|
|
alt: `Placeholder Image ${i+1}`, |
|
|
author: 'Placeholder Author', |
|
|
authorUrl: '#' |
|
|
}))); |
|
|
} finally { |
|
|
setIsLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
fetchImages(); |
|
|
}, [apiSource]); |
|
|
|
|
|
useEffect(() => { |
|
|
if (isAutoPlaying && images.length > 0) { |
|
|
timerRef.current = setInterval(() => { |
|
|
setCurrentIndex(prev => (prev + 1) % images.length); |
|
|
}, 5000); |
|
|
} |
|
|
return () => clearInterval(timerRef.current); |
|
|
}, [isAutoPlaying, images.length]); |
|
|
|
|
|
const goToPrev = () => { |
|
|
setCurrentIndex(prev => (prev - 1 + images.length) % images.length); |
|
|
resetTimer(); |
|
|
}; |
|
|
|
|
|
const goToNext = () => { |
|
|
setCurrentIndex(prev => (prev + 1) % images.length); |
|
|
resetTimer(); |
|
|
}; |
|
|
|
|
|
const goToIndex = (index) => { |
|
|
setCurrentIndex(index); |
|
|
resetTimer(); |
|
|
}; |
|
|
|
|
|
const resetTimer = () => { |
|
|
if (isAutoPlaying) { |
|
|
clearInterval(timerRef.current); |
|
|
timerRef.current = setInterval(() => { |
|
|
setCurrentIndex(prev => (prev + 1) % images.length); |
|
|
}, 5000); |
|
|
} |
|
|
}; |
|
|
|
|
|
const toggleAutoPlay = () => { |
|
|
setIsAutoPlaying(prev => !prev); |
|
|
}; |
|
|
|
|
|
const toggleDarkMode = () => { |
|
|
setDarkMode(prev => !prev); |
|
|
document.body.classList.toggle('dark'); |
|
|
}; |
|
|
|
|
|
if (error) { |
|
|
return ( |
|
|
<div className="flex flex-col items-center justify-center min-h-screen p-4"> |
|
|
<div className="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 p-4 rounded-lg max-w-md"> |
|
|
<div className="flex items-center"> |
|
|
<i data-feather="alert-triangle" className="mr-2"></i> |
|
|
<h3 className="font-bold">Error Loading Images</h3> |
|
|
</div> |
|
|
<p className="mt-2">{error}</p> |
|
|
<p className="mt-2 text-sm">Using static placeholder images.</p> |
|
|
<button |
|
|
onClick={fetchImages} |
|
|
className="mt-4 bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md transition-colors" |
|
|
> |
|
|
Retry |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
return ( |
|
|
<div className="min-h-screen flex flex-col items-center justify-center p-4"> |
|
|
<div className="w-full max-w-4xl"> |
|
|
<div className="flex justify-between items-center mb-6"> |
|
|
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">PixelFlow Carousel</h1> |
|
|
<div className="flex space-x-4"> |
|
|
<div className="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-white px-3 py-2 rounded-md"> |
|
|
Static Photos |
|
|
</div> |
|
|
<button |
|
|
onClick={toggleDarkMode} |
|
|
className="p-2 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200" |
|
|
> |
|
|
<i data-feather={darkMode ? "sun" : "moon"}></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div className="relative overflow-hidden rounded-xl shadow-xl"> |
|
|
{isLoading ? ( |
|
|
<div className="aspect-video w-full skeleton rounded-xl"></div> |
|
|
) : ( |
|
|
<div |
|
|
ref={carouselRef} |
|
|
className="relative aspect-video w-full overflow-hidden" |
|
|
> |
|
|
{images.map((image, index) => ( |
|
|
<div |
|
|
key={index} |
|
|
className={`absolute inset-0 flex items-center justify-center carousel-transition ${index === currentIndex ? 'opacity-100' : 'opacity-0 pointer-events-none'}`} |
|
|
> |
|
|
<div className="relative w-full h-full group"> |
|
|
<img |
|
|
src={image.url} |
|
|
alt={image.alt} |
|
|
className="w-full h-full object-cover zoom-effect" |
|
|
/> |
|
|
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-300 flex items-end p-6"> |
|
|
<div className="transform translate-y-8 group-hover:translate-y-0 transition-transform duration-300"> |
|
|
<h3 className="text-white text-xl font-bold">{image.alt}</h3> |
|
|
<a |
|
|
href={image.authorUrl} |
|
|
target="_blank" |
|
|
rel="noopener noreferrer" |
|
|
className="text-gray-200 hover:text-white text-sm" |
|
|
> |
|
|
Photo by {image.author} |
|
|
</a> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Navigation Arrows */} |
|
|
<button |
|
|
onClick={goToPrev} |
|
|
className="absolute left-4 top-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 bg-opacity-80 dark:bg-opacity-80 p-2 rounded-full shadow-md hover:bg-opacity-100 transition-all" |
|
|
disabled={isLoading} |
|
|
> |
|
|
<i data-feather="chevron-left" className="text-gray-800 dark:text-white"></i> |
|
|
</button> |
|
|
<button |
|
|
onClick={goToNext} |
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 bg-opacity-80 dark:bg-opacity-80 p-2 rounded-full shadow-md hover:bg-opacity-100 transition-all" |
|
|
disabled={isLoading} |
|
|
> |
|
|
<i data-feather="chevron-right" className="text-gray-800 dark:text-white"></i> |
|
|
</button> |
|
|
|
|
|
{/* Indicators */} |
|
|
<div className="absolute bottom-4 left-0 right-0 flex justify-center space-x-2"> |
|
|
{images.map((_, index) => ( |
|
|
<button |
|
|
key={index} |
|
|
onClick={() => goToIndex(index)} |
|
|
className={`w-3 h-3 rounded-full ${index === currentIndex ? 'bg-white' : 'bg-white bg-opacity-50'} transition-all`} |
|
|
disabled={isLoading} |
|
|
></button> |
|
|
))} |
|
|
</div> |
|
|
|
|
|
{/* Play/Pause Button */} |
|
|
<button |
|
|
onClick={toggleAutoPlay} |
|
|
className="absolute top-4 right-4 bg-white dark:bg-gray-800 bg-opacity-80 dark:bg-opacity-80 p-2 rounded-full shadow-md hover:bg-opacity-100 transition-all" |
|
|
> |
|
|
<i data-feather={isAutoPlaying ? "pause" : "play"} className="text-gray-800 dark:text-white"></i> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div className="mt-6 text-center text-gray-600 dark:text-gray-400"> |
|
|
<p>Browse the static image carousel using navigation controls</p> |
|
|
<div className="mt-2 flex justify-center space-x-4"> |
|
|
<button |
|
|
onClick={fetchImages} |
|
|
className="flex items-center text-sm bg-gray-100 dark:bg-gray-700 px-3 py-1 rounded-md" |
|
|
> |
|
|
<i data-feather="refresh-cw" className="w-4 h-4 mr-1"></i> |
|
|
Refresh Images |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
const App = () => { |
|
|
return ( |
|
|
<div> |
|
|
<ImageCarousel /> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root')); |
|
|
root.render(<App />); |
|
|
|
|
|
|
|
|
feather.replace(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|