Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Video Streaming Platform</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: #0f0f0f; | |
| color: #fff; | |
| line-height: 1.6; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 0 20px; | |
| } | |
| /* Header */ | |
| header { | |
| background: #212121; | |
| padding: 1rem 0; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| border-bottom: 1px solid #333; | |
| } | |
| .header-content { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| } | |
| .logo { | |
| font-size: 1.8rem; | |
| font-weight: bold; | |
| color: #ff6b6b; | |
| cursor: pointer; | |
| transition: color 0.3s ease; | |
| } | |
| .logo:hover { | |
| color: #ff5252; | |
| } | |
| .search-container { | |
| flex: 1; | |
| max-width: 500px; | |
| position: relative; | |
| } | |
| #searchInput { | |
| width: 100%; | |
| padding: 12px 20px; | |
| background: #333; | |
| border: 2px solid #444; | |
| border-radius: 25px; | |
| color: #fff; | |
| font-size: 16px; | |
| transition: all 0.3s ease; | |
| } | |
| #searchInput:focus { | |
| outline: none; | |
| border-color: #ff6b6b; | |
| background: #404040; | |
| } | |
| #searchInput::placeholder { | |
| color: #aaa; | |
| } | |
| /* Main Content */ | |
| main { | |
| min-height: calc(100vh - 200px); | |
| padding: 2rem 0; | |
| } | |
| /* Video Grid */ | |
| .video-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); | |
| gap: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .video-card { | |
| background: #1e1e1e; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| border: 2px solid transparent; | |
| } | |
| .video-card:hover { | |
| transform: translateY(-8px); | |
| border-color: #ff6b6b; | |
| box-shadow: 0 12px 30px rgba(255, 107, 107, 0.3); | |
| } | |
| .video-thumbnail { | |
| width: 100%; | |
| height: 180px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .play-icon { | |
| width: 60px; | |
| height: 60px; | |
| background: rgba(255, 255, 255, 0.9); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 24px; | |
| color: #333; | |
| transition: all 0.3s ease; | |
| } | |
| .video-card:hover .play-icon { | |
| transform: scale(1.1); | |
| background: #ff6b6b; | |
| color: #fff; | |
| } | |
| .video-info { | |
| padding: 1rem; | |
| } | |
| .video-title { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| color: #fff; | |
| margin-bottom: 0.5rem; | |
| } | |
| .video-meta { | |
| color: #aaa; | |
| font-size: 0.9rem; | |
| } | |
| /* Video Player Page */ | |
| .player-container { | |
| display: none; | |
| } | |
| .player-container.active { | |
| display: block; | |
| } | |
| .video-player-section { | |
| background: #1e1e1e; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| margin-bottom: 2rem; | |
| } | |
| .video-player { | |
| width: 100%; | |
| max-width: 900px; | |
| height: auto; | |
| border-radius: 8px; | |
| margin-bottom: 1rem; | |
| } | |
| .video-details h2 { | |
| font-size: 1.5rem; | |
| margin-bottom: 1rem; | |
| color: #fff; | |
| } | |
| .back-button { | |
| background: #ff6b6b; | |
| color: #fff; | |
| border: none; | |
| padding: 12px 24px; | |
| border-radius: 25px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: all 0.3s ease; | |
| margin-bottom: 1rem; | |
| } | |
| .back-button:hover { | |
| background: #ff5252; | |
| transform: translateY(-2px); | |
| } | |
| .recommendations { | |
| margin-top: 2rem; | |
| } | |
| .recommendations h3 { | |
| margin-bottom: 1rem; | |
| color: #fff; | |
| font-size: 1.3rem; | |
| } | |
| .recommendations-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); | |
| gap: 1rem; | |
| } | |
| .recommendation-card { | |
| background: #2a2a2a; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| border: 1px solid #444; | |
| } | |
| .recommendation-card:hover { | |
| background: #333; | |
| border-color: #ff6b6b; | |
| } | |
| .recommendation-title { | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| } | |
| /* Pagination */ | |
| .pagination { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 0.5rem; | |
| margin: 2rem 0; | |
| flex-wrap: wrap; | |
| } | |
| .pagination button { | |
| background: #333; | |
| color: #fff; | |
| border: none; | |
| padding: 10px 15px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 14px; | |
| } | |
| .pagination button:hover:not(:disabled) { | |
| background: #ff6b6b; | |
| transform: translateY(-2px); | |
| } | |
| .pagination button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .pagination button.active { | |
| background: #ff6b6b; | |
| } | |
| .pagination-info { | |
| color: #aaa; | |
| margin: 0 1rem; | |
| font-size: 14px; | |
| } | |
| /* No Results */ | |
| .no-results { | |
| text-align: center; | |
| padding: 3rem; | |
| color: #aaa; | |
| } | |
| .no-results h3 { | |
| font-size: 1.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| .no-results p { | |
| font-size: 1.1rem; | |
| } | |
| /* Loading */ | |
| .loading { | |
| text-align: center; | |
| padding: 3rem; | |
| color: #aaa; | |
| } | |
| .loading-spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 3px solid #333; | |
| border-top: 3px solid #ff6b6b; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 1rem; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 768px) { | |
| .header-content { | |
| flex-direction: column; | |
| text-align: center; | |
| } | |
| .search-container { | |
| max-width: 100%; | |
| } | |
| .video-grid { | |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
| gap: 1rem; | |
| } | |
| .recommendations-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .pagination { | |
| gap: 0.25rem; | |
| } | |
| .pagination button { | |
| padding: 8px 12px; | |
| font-size: 12px; | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .video-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .video-thumbnail { | |
| height: 200px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="container"> | |
| <div class="header-content"> | |
| <div class="logo" onclick="showVideoList()">📺 VideoHub</div> | |
| <div class="search-container"> | |
| <input type="text" id="searchInput" placeholder="Search videos..." oninput="handleSearch()"> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <main> | |
| <div class="container"> | |
| <!-- Video List View --> | |
| <div id="videoListContainer"> | |
| <div id="loadingIndicator" class="loading"> | |
| <div class="loading-spinner"></div> | |
| <p>Loading videos...</p> | |
| </div> | |
| <div id="videoGrid" class="video-grid"></div> | |
| <div id="noResults" class="no-results" style="display: none;"> | |
| <h3>No videos found</h3> | |
| <p>Try adjusting your search terms</p> | |
| </div> | |
| <div id="paginationContainer" class="pagination"></div> | |
| </div> | |
| <!-- Video Player View --> | |
| <div id="videoPlayerContainer" class="player-container"> | |
| <button class="back-button" onclick="showVideoList()">← Back to Videos</button> | |
| <div class="video-player-section"> | |
| <video id="videoPlayer" class="video-player" controls> | |
| Your browser does not support the video tag. | |
| </video> | |
| <div class="video-details"> | |
| <h2 id="currentVideoTitle">Video Title</h2> | |
| </div> | |
| </div> | |
| <div class="recommendations"> | |
| <h3>Up Next</h3> | |
| <div id="recommendationsGrid" class="recommendations-grid"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| // Global state | |
| let allVideos = []; | |
| let filteredVideos = []; | |
| let currentPage = 1; | |
| const videosPerPage = 12; | |
| let currentVideoIndex = -1; | |
| let searchQuery = ''; | |
| // Sample data (replace with actual database.json loading) | |
| const sampleData = [ | |
| { "title": "E01 - Introduction", "url": "https://example.com/video1.mkv" }, | |
| { "title": "E02 - Getting Started", "url": "https://example.com/video2.mkv" }, | |
| { "title": "E03 - Advanced Concepts", "url": "https://example.com/video3.mkv" }, | |
| { "title": "E04 - Best Practices", "url": "https://example.com/video4.mkv" }, | |
| { "title": "E05 - Case Studies", "url": "https://example.com/video5.mkv" }, | |
| { "title": "E06 - Implementation", "url": "https://example.com/video6.mkv" }, | |
| { "title": "E07 - Testing", "url": "https://example.com/video7.mkv" }, | |
| { "title": "E08 - Deployment", "url": "https://example.com/video8.mkv" }, | |
| { "title": "E09 - Monitoring", "url": "https://example.com/video9.mkv" }, | |
| { "title": "E10 - Optimization", "url": "https://example.com/video10.mkv" }, | |
| { "title": "E11 - Security", "url": "https://example.com/video11.mkv" }, | |
| { "title": "E12 - Maintenance", "url": "https://example.com/video12.mkv" }, | |
| { "title": "E13 - Troubleshooting", "url": "https://example.com/video13.mkv" }, | |
| { "title": "E14 - Future Trends", "url": "https://example.com/video14.mkv" }, | |
| { "title": "E15 - Conclusion", "url": "https://example.com/video15.mkv" } | |
| ]; | |
| // Initialize the application | |
| async function initApp() { | |
| try { | |
| // Try to load from database.json, fallback to sample data | |
| await loadVideos(); | |
| setupEventListeners(); | |
| handleURLHash(); | |
| } catch (error) { | |
| console.error('Error initializing app:', error); | |
| // Use sample data as fallback | |
| allVideos = [...sampleData]; | |
| filteredVideos = [...allVideos]; | |
| hideLoading(); | |
| renderVideoGrid(); | |
| renderPagination(); | |
| } | |
| } | |
| // Load videos from database.json | |
| async function loadVideos() { | |
| try { | |
| const response = await fetch('database.json'); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| allVideos = data.sort((a, b) => a.title.localeCompare(b.title)); | |
| } else { | |
| throw new Error('Failed to load database.json'); | |
| } | |
| } catch (error) { | |
| console.warn('Could not load database.json, using sample data:', error); | |
| allVideos = [...sampleData]; | |
| } | |
| filteredVideos = [...allVideos]; | |
| hideLoading(); | |
| renderVideoGrid(); | |
| renderPagination(); | |
| } | |
| // Setup event listeners | |
| function setupEventListeners() { | |
| // Handle browser back/forward | |
| window.addEventListener('popstate', handleURLHash); | |
| // Handle video player events | |
| const videoPlayer = document.getElementById('videoPlayer'); | |
| videoPlayer.addEventListener('error', handleVideoError); | |
| videoPlayer.addEventListener('loadedmetadata', handleVideoLoaded); | |
| } | |
| // Handle URL hash changes | |
| function handleURLHash() { | |
| const hash = window.location.hash; | |
| if (hash.startsWith('#play=')) { | |
| const videoTitle = decodeURIComponent(hash.substring(6)); | |
| const videoIndex = allVideos.findIndex(v => v.title === videoTitle); | |
| if (videoIndex !== -1) { | |
| playVideo(videoIndex); | |
| } else { | |
| showVideoList(); | |
| } | |
| } else { | |
| showVideoList(); | |
| } | |
| } | |
| // Hide loading indicator | |
| function hideLoading() { | |
| document.getElementById('loadingIndicator').style.display = 'none'; | |
| } | |
| // Handle search input | |
| function handleSearch() { | |
| const query = document.getElementById('searchInput').value.toLowerCase().trim(); | |
| searchQuery = query; | |
| if (query === '') { | |
| filteredVideos = [...allVideos]; | |
| } else { | |
| filteredVideos = allVideos.filter(video => | |
| video.title.toLowerCase().includes(query) | |
| ); | |
| } | |
| currentPage = 1; | |
| renderVideoGrid(); | |
| renderPagination(); | |
| } | |
| // Render video grid | |
| function renderVideoGrid() { | |
| const grid = document.getElementById('videoGrid'); | |
| const noResults = document.getElementById('noResults'); | |
| if (filteredVideos.length === 0) { | |
| grid.innerHTML = ''; | |
| noResults.style.display = 'block'; | |
| return; | |
| } | |
| noResults.style.display = 'none'; | |
| const startIndex = (currentPage - 1) * videosPerPage; | |
| const endIndex = startIndex + videosPerPage; | |
| const videosToShow = filteredVideos.slice(startIndex, endIndex); | |
| grid.innerHTML = videosToShow.map((video, index) => { | |
| const actualIndex = allVideos.findIndex(v => v.title === video.title); | |
| return ` | |
| <div class="video-card" onclick="playVideo(${actualIndex})"> | |
| <div class="video-thumbnail"> | |
| <div class="play-icon">▶</div> | |
| </div> | |
| <div class="video-info"> | |
| <div class="video-title">${escapeHtml(video.title)}</div> | |
| <div class="video-meta">Click to play</div> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| // Render pagination | |
| function renderPagination() { | |
| const container = document.getElementById('paginationContainer'); | |
| const totalPages = Math.ceil(filteredVideos.length / videosPerPage); | |
| if (totalPages <= 1) { | |
| container.innerHTML = ''; | |
| return; | |
| } | |
| let paginationHTML = ''; | |
| // Previous button | |
| paginationHTML += ` | |
| <button onclick="changePage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}> | |
| ← Previous | |
| </button> | |
| `; | |
| // Page numbers | |
| const maxVisiblePages = 5; | |
| let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); | |
| let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); | |
| if (endPage - startPage + 1 < maxVisiblePages) { | |
| startPage = Math.max(1, endPage - maxVisiblePages + 1); | |
| } | |
| if (startPage > 1) { | |
| paginationHTML += `<button onclick="changePage(1)">1</button>`; | |
| if (startPage > 2) { | |
| paginationHTML += `<span class="pagination-info">...</span>`; | |
| } | |
| } | |
| for (let i = startPage; i <= endPage; i++) { | |
| paginationHTML += ` | |
| <button onclick="changePage(${i})" ${i === currentPage ? 'class="active"' : ''}> | |
| ${i} | |
| </button> | |
| `; | |
| } | |
| if (endPage < totalPages) { | |
| if (endPage < totalPages - 1) { | |
| paginationHTML += `<span class="pagination-info">...</span>`; | |
| } | |
| paginationHTML += `<button onclick="changePage(${totalPages})">${totalPages}</button>`; | |
| } | |
| // Next button | |
| paginationHTML += ` | |
| <button onclick="changePage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}> | |
| Next → | |
| </button> | |
| `; | |
| // Page info | |
| const startItem = (currentPage - 1) * videosPerPage + 1; | |
| const endItem = Math.min(currentPage * videosPerPage, filteredVideos.length); | |
| paginationHTML += ` | |
| <span class="pagination-info"> | |
| ${startItem}-${endItem} of ${filteredVideos.length} videos | |
| </span> | |
| `; | |
| container.innerHTML = paginationHTML; | |
| } | |
| // Change page | |
| function changePage(page) { | |
| const totalPages = Math.ceil(filteredVideos.length / videosPerPage); | |
| if (page >= 1 && page <= totalPages) { | |
| currentPage = page; | |
| renderVideoGrid(); | |
| renderPagination(); | |
| // Scroll to top | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| } | |
| // Play video | |
| function playVideo(videoIndex) { | |
| if (videoIndex < 0 || videoIndex >= allVideos.length) return; | |
| currentVideoIndex = videoIndex; | |
| const video = allVideos[videoIndex]; | |
| // Update URL hash | |
| window.history.pushState(null, null, `#play=${encodeURIComponent(video.title)}`); | |
| // Hide video list, show player | |
| document.getElementById('videoListContainer').style.display = 'none'; | |
| document.getElementById('videoPlayerContainer').classList.add('active'); | |
| // Set up video player | |
| const videoPlayer = document.getElementById('videoPlayer'); | |
| videoPlayer.src = video.url; | |
| // Update video title | |
| document.getElementById('currentVideoTitle').textContent = video.title; | |
| // Generate recommendations | |
| generateRecommendations(); | |
| // Scroll to top | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| // Generate recommendations | |
| function generateRecommendations() { | |
| const grid = document.getElementById('recommendationsGrid'); | |
| const recommendations = []; | |
| // Get next 5 videos in sequence | |
| for (let i = 1; i <= 5; i++) { | |
| const nextIndex = currentVideoIndex + i; | |
| if (nextIndex < allVideos.length) { | |
| recommendations.push({ video: allVideos[nextIndex], index: nextIndex }); | |
| } | |
| } | |
| // If we don't have enough recommendations, add some from the beginning | |
| let startIndex = 0; | |
| while (recommendations.length < 5 && startIndex < allVideos.length) { | |
| if (startIndex !== currentVideoIndex && | |
| !recommendations.some(r => r.index === startIndex)) { | |
| recommendations.push({ video: allVideos[startIndex], index: startIndex }); | |
| } | |
| startIndex++; | |
| } | |
| grid.innerHTML = recommendations.map(({ video, index }) => ` | |
| <div class="recommendation-card" onclick="playVideo(${index})"> | |
| <div class="recommendation-title">${escapeHtml(video.title)}</div> | |
| <div class="video-meta">Click to play</div> | |
| </div> | |
| `).join(''); | |
| } | |
| // Show video list | |
| function showVideoList() { | |
| // Update URL | |
| window.history.pushState(null, null, window.location.pathname); | |
| // Hide player, show list | |
| document.getElementById('videoPlayerContainer').classList.remove('active'); | |
| document.getElementById('videoListContainer').style.display = 'block'; | |
| // Pause current video | |
| const videoPlayer = document.getElementById('videoPlayer'); | |
| videoPlayer.pause(); | |
| } | |
| // Handle video errors | |
| function handleVideoError(event) { | |
| console.error('Video error:', event); | |
| alert('Error loading video. Please check the video URL or try again later.'); | |
| } | |
| // Handle video loaded | |
| function handleVideoLoaded(event) { | |
| console.log('Video loaded successfully'); | |
| } | |
| // Utility function to escape HTML | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // Initialize the app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', initApp); | |
| </script> | |
| </body> | |
| </html> |