diff --git a/frontend/src/components/MediaCarousel.css b/frontend/src/components/MediaCarousel.css new file mode 100644 index 0000000..6662600 --- /dev/null +++ b/frontend/src/components/MediaCarousel.css @@ -0,0 +1,155 @@ +.media-carousel { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.95); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.carousel-close-btn { + position: absolute; + top: 1rem; + right: 1rem; + background-color: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 10001; + transition: background-color 0.2s ease, transform 0.2s ease; +} + +.carousel-close-btn:hover { + background-color: white; + transform: scale(1.1); +} + +.carousel-counter { + position: absolute; + top: 1rem; + left: 50%; + transform: translateX(-50%); + color: white; + background-color: rgba(0, 0, 0, 0.6); + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.9rem; + z-index: 10001; +} + +.carousel-info { + position: absolute; + bottom: 1rem; + left: 50%; + transform: translateX(-50%); + color: white; + background-color: rgba(0, 0, 0, 0.6); + padding: 0.75rem 1.5rem; + border-radius: 8px; + z-index: 10001; + max-width: 80%; + text-align: center; +} + +.carousel-nav-btn { + position: absolute; + top: 50%; + transform: translateY(-50%); + background-color: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 50%; + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 10001; + transition: background-color 0.2s ease, transform 0.2s ease; +} + +.carousel-nav-btn:hover { + background-color: white; + transform: translateY(-50%) scale(1.1); +} + +.carousel-prev-btn { + left: 1rem; +} + +.carousel-next-btn { + right: 1rem; +} + +.carousel-media-container { + max-width: 90vw; + max-height: 90vh; + display: flex; + align-items: center; + justify-content: center; +} + +.carousel-media { + max-width: 100%; + max-height: 90vh; + object-fit: contain; + border-radius: 8px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); +} + +/* Mobile adjustments */ +@media (max-width: 768px) { + .media-carousel { + padding: 1rem; + } + + .carousel-nav-btn { + width: 40px; + height: 40px; + } + + .carousel-close-btn { + top: 0.5rem; + right: 0.5rem; + } + + .carousel-counter { + top: 0.5rem; + font-size: 0.8rem; + padding: 0.4rem 0.8rem; + } + + .carousel-info { + bottom: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.85rem; + max-width: 90%; + } + + .carousel-prev-btn { + left: 0.5rem; + } + + .carousel-next-btn { + right: 0.5rem; + } +} + +/* Keyboard navigation hint */ +@media (min-width: 769px) { + .carousel-nav-btn::after { + content: ''; + position: absolute; + inset: -5px; + } +} diff --git a/frontend/src/components/MediaCarousel.jsx b/frontend/src/components/MediaCarousel.jsx new file mode 100644 index 0000000..519685e --- /dev/null +++ b/frontend/src/components/MediaCarousel.jsx @@ -0,0 +1,213 @@ +import { useState, useEffect, useRef } from 'react' +import { format } from 'date-fns' +import './MediaCarousel.css' + +const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8081/api' + +function MediaCarousel({ mediaItems, initialIndex, onClose, transcodeVideos = new Set() }) { + const [currentIndex, setCurrentIndex] = useState(initialIndex) + const [touchStart, setTouchStart] = useState(null) + const [touchEnd, setTouchEnd] = useState(null) + const videoRef = useRef(null) + + const currentItem = mediaItems[currentIndex] + const isVideo = currentItem?.media_type?.startsWith('video/') + const shouldTranscode = transcodeVideos.has(currentItem?.id) + + useEffect(() => { + // Prevent body scroll when carousel is open + document.body.style.overflow = 'hidden' + + // Hide UI elements that might appear over the carousel + const datePickers = document.querySelectorAll('.react-datepicker-popper') + datePickers.forEach(picker => { + picker.style.display = 'none' + }) + + const dateFilterContainer = document.querySelector('.date-filter-container') + if (dateFilterContainer) { + dateFilterContainer.style.visibility = 'hidden' + } + + const header = document.querySelector('header') + if (header) { + header.style.visibility = 'hidden' + } + + return () => { + document.body.style.overflow = 'unset' + + // Restore visibility of hidden elements + const datePickers = document.querySelectorAll('.react-datepicker-popper') + datePickers.forEach(picker => { + picker.style.display = '' + }) + + const dateFilterContainer = document.querySelector('.date-filter-container') + if (dateFilterContainer) { + dateFilterContainer.style.visibility = '' + } + + const header = document.querySelector('header') + if (header) { + header.style.visibility = '' + } + } + }, []) + + useEffect(() => { + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + onClose() + } else if (e.key === 'ArrowLeft') { + handlePrevious() + } else if (e.key === 'ArrowRight') { + handleNext() + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [currentIndex]) + + const handlePrevious = () => { + if (currentIndex > 0) { + setCurrentIndex(currentIndex - 1) + } + } + + const handleNext = () => { + if (currentIndex < mediaItems.length - 1) { + setCurrentIndex(currentIndex + 1) + } + } + + // Touch handlers for swipe gestures + const handleTouchStart = (e) => { + setTouchEnd(null) + setTouchStart(e.targetTouches[0].clientX) + } + + const handleTouchMove = (e) => { + setTouchEnd(e.targetTouches[0].clientX) + } + + const handleTouchEnd = () => { + if (!touchStart || !touchEnd) return + + const distance = touchStart - touchEnd + const isLeftSwipe = distance > 50 + const isRightSwipe = distance < -50 + + if (isLeftSwipe) { + handleNext() + } + if (isRightSwipe) { + handlePrevious() + } + } + + const formatTime = (date) => { + return format(new Date(date), 'MMM d, yyyy h:mm a') + } + + return ( +
+ {mediaItems.length === 0 + ? 'No photos or videos found in this conversation' + : 'No browser-compatible photos or videos found in this conversation'} +
+