| <!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> |
|
|