Created
August 22, 2025 12:57
-
-
Save erikusaj/b162ded2c3de826c3f3db2c1b3e08ec0 to your computer and use it in GitHub Desktop.
simple tv grid
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>4-Column TV Navigation with Wrapping and Dates</title> | |
| <!-- Tailwind CSS CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| /* Custom styles for the navigation columns and items */ | |
| .column { | |
| flex-shrink: 0; | |
| width: 300px; | |
| max-height: 90vh; | |
| position: relative; | |
| overflow: hidden; /* Hide overflow to create the wrapping effect */ | |
| } | |
| .column ul { | |
| list-style-type: none; | |
| padding: 0; | |
| margin: 0; | |
| transition: transform 0.3s ease-in-out; /* Smooth transition for shifting */ | |
| } | |
| .column li { | |
| padding: 1rem; | |
| margin-bottom: 0.5rem; | |
| border-radius: 0.5rem; | |
| transition: all 0.2s ease-in-out; | |
| cursor: pointer; | |
| text-align: left; | |
| background-color: #374151; /* Gray 700 */ | |
| min-height: 50px; /* Ensure a consistent item height */ | |
| display: flex; | |
| align-items: center; | |
| } | |
| /* Styles for the focused item in any column */ | |
| .column li.focused { | |
| border: 4px solid #6366f1; /* Indigo 500 */ | |
| background-color: #4f46e5; /* Indigo 600 */ | |
| color: white; | |
| transform: scale(1.05); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| } | |
| /* Styles for the central column to make the focused item stand out */ | |
| .central-column li { | |
| background-color: #1f2937; /* Gray 800 */ | |
| min-height: 80px; | |
| margin-bottom: 0.75rem; | |
| } | |
| .central-column li.focused { | |
| min-height: 200px; | |
| width: 350px; | |
| justify-content: space-between; | |
| background-color: #f3f4f6; /* Gray 100 */ | |
| color: #1f2937; /* Gray 900 */ | |
| padding: 1.5rem; | |
| } | |
| .central-column .movie-details { | |
| display: none; | |
| } | |
| .central-column li.focused .movie-details { | |
| display: block; | |
| } | |
| .central-column .movie-title { | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| } | |
| .central-column .movie-info { | |
| font-size: 0.875rem; | |
| color: #4b5563; /* Gray 600 */ | |
| } | |
| /* Hide notification by default */ | |
| .notification-popup.hidden { | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-gray-100 flex items-center justify-center min-h-screen p-4 font-sans"> | |
| <div class="container mx-auto flex items-start justify-center space-x-8 h-[90vh]"> | |
| <!-- Left Column (Index 0) --> | |
| <span id="column-0" class="column"> | |
| <ul> | |
| <li data-column="0" data-index="0">pridruženi</li> | |
| <li data-column="0" data-index="1">izbrani</li> | |
| <li data-column="0" data-index="2">dokumentarni</li> | |
| <li data-column="0" data-index="3">filmski</li> | |
| <li data-column="0" data-index="4">otroški</li> | |
| <li data-column="0" data-index="5">glasbeni</li> | |
| <li data-column="0" data-index="6">lokalni</li> | |
| <li data-column="0" data-index="7">informativni</li> | |
| <li data-column="0" data-index="8">balkan</li> | |
| <li data-column="0" data-index="9">odrasli</li> | |
| </ul> | |
| </span> | |
| <!-- Center Column (Index 1) --> | |
| <span id="column-1" class="column central-column w-[400px]"> | |
| <ul id="central-list"> | |
| <li data-column="1" data-index="0"> | |
| <img src="https://placehold.co/100x150/374151/E5E7EB?text=234" class="rounded-lg w-auto h-auto"> | |
| <span>DKino - New</span> | |
| </li> | |
| <li data-column="1" data-index="1"> | |
| <img src="https://placehold.co/100x150/374151/E5E7EB?text=234" class="rounded-lg w-auto h-auto"> | |
| <div class="movie-details"> | |
| <div class="movie-title">Superman 2</div> | |
| <div class="movie-info">Drama • IMDB 7,3</div> | |
| </div> | |
| </li> | |
| <li data-column="1" data-index="2"> | |
| <img src="https://placehold.co/100x150/374151/E5E7EB?text=234" class="rounded-lg w-auto h-auto"> | |
| <span>Ljubica</span> | |
| </li> | |
| <li data-column="1" data-index="3"> | |
| <img src="https://placehold.co/100x150/374151/E5E7EB?text=234" class="rounded-lg w-auto h-auto"> | |
| <span>Ameriške prevare</span> | |
| </li> | |
| <li data-column="1" data-index="4"> | |
| <img src="https://placehold.co/100x150/374151/E5E7EB?text=234" class="rounded-lg w-auto h-auto"> | |
| <span>Julie & Julia</span> | |
| </li> | |
| </ul> | |
| </span> | |
| <!-- New Date Column (Index 2) --> | |
| <span id="column-2" class="column"> | |
| <ul> | |
| <!-- Dates will be populated here by JavaScript --> | |
| </ul> | |
| </span> | |
| <!-- Right Column (Index 3) --> | |
| <span id="column-3" class="column"> | |
| <ul> | |
| <li data-column="3" data-index="0">7:10 Cernobil</li> | |
| <li data-column="3" data-index="1">8:00 Cernobil</li> | |
| <li data-column="3" data-index="2">9:00 Drops of God</li> | |
| <li data-column="3" data-index="3">10:00 72 Hours: True Crime</li> | |
| <li data-column="3" data-index="4">11:00 72 Hours: True Crime</li> | |
| <li data-column="3" data-index="5">12:10 Zgodbe iz skrajlike</li> | |
| <li data-column="3" data-index="6">13:00 Traktor, ljubezen in rock'n'roll</li> | |
| <li data-column="3" data-index="7">14:00 Drops of God</li> | |
| </ul> | |
| </span> | |
| </div> | |
| <!-- Notification Popup --> | |
| <div id="notification-popup" class="notification-popup hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"> | |
| <div class="bg-white p-8 rounded-lg shadow-xl text-center max-w-sm w-full"> | |
| <h2 class="text-2xl font-bold text-gray-800 mb-4">Action Confirmed!</h2> | |
| <p class="text-gray-700 mb-6">You pressed Enter on: <span id="selected-item-text" class="font-semibold text-indigo-600"></span></p> | |
| <button id="close-notification" class="px-6 py-3 bg-indigo-500 text-white rounded-md hover:bg-indigo-600 transition duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-opacity-75"> | |
| Got It! | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const columns = document.querySelectorAll('.column'); | |
| const notificationPopup = document.getElementById('notification-popup'); | |
| const closeNotificationBtn = document.getElementById('close-notification'); | |
| const selectedItemText = document.getElementById('selected-item-text'); | |
| const days = ['Nedelja', 'Ponedeljek', 'Torek', 'Sreda', 'Četrtek', 'Petek', 'Sobota']; | |
| // Function to generate and populate the date list | |
| function populateDates() { | |
| const dateList = document.querySelector('#column-2 ul'); | |
| const today = new Date(); | |
| const items = []; | |
| // Add 14 days in the past | |
| for (let i = 14; i >= 1; i--) { | |
| const d = new Date(today); | |
| d.setDate(today.getDate() - i); | |
| items.push(d); | |
| } | |
| // Add today | |
| items.push(today); | |
| // Add one day in the future | |
| const dFuture = new Date(today); | |
| dFuture.setDate(today.getDate() + 1); | |
| items.push(dFuture); | |
| dateList.innerHTML = items.map((date, index) => { | |
| const dayOfWeek = days[date.getDay()]; | |
| const month = date.getMonth() + 1; | |
| const day = date.getDate(); | |
| return `<li data-column="2" data-index="${index}">${day}. ${month}. ${dayOfWeek}</li>`; | |
| }).join(''); | |
| return items.length; | |
| } | |
| const dateItemsCount = populateDates(); | |
| let currentColumnIndex = 1; // Start with the central column | |
| const itemsInColumn = []; | |
| // Populate the itemsInColumn array after populating the dates | |
| for (let i = 0; i < columns.length; i++) { | |
| itemsInColumn.push(document.querySelectorAll(`#column-${i} li`)); | |
| } | |
| let currentItemIndex = [0, 0, 0, 0]; // 4 columns, initial selected index is 0 for each | |
| // Set the initial focus for the date column to today | |
| // The "today" item is the 15th item (index 14) in the list of 16 items | |
| currentItemIndex[2] = 14; | |
| // Function to update the focused item and manage the visual wrap-around | |
| function updateFocus() { | |
| // Remove 'focused' class from all items | |
| document.querySelectorAll('li.focused').forEach(item => item.classList.remove('focused')); | |
| const currentItems = itemsInColumn[currentColumnIndex]; | |
| if (currentItems.length === 0) return; | |
| const currentIndex = currentItemIndex[currentColumnIndex]; | |
| const focusedItem = currentItems[currentIndex]; | |
| focusedItem.classList.add('focused'); | |
| // Get the list container for the current column | |
| const listContainer = document.querySelector(`#column-${currentColumnIndex} ul`); | |
| // Calculate the offset needed to center the focused item | |
| const containerHeight = listContainer.parentElement.clientHeight; | |
| const itemHeight = focusedItem.offsetHeight + parseFloat(getComputedStyle(focusedItem).marginBottom); | |
| const offset = (containerHeight / 2) - (itemHeight / 2); | |
| // Calculate the translateY value to center the focused item | |
| // This accounts for the cumulative height of items before the focused one | |
| const translateYValue = - (currentIndex * itemHeight) + offset; | |
| listContainer.style.transform = `translateY(${translateYValue}px)`; | |
| } | |
| // Function to show the notification popup | |
| function showNotification(itemElement) { | |
| selectedItemText.textContent = itemElement.textContent.trim(); | |
| notificationPopup.classList.remove('hidden'); | |
| } | |
| // Function to hide the notification popup | |
| function hideNotification() { | |
| notificationPopup.classList.add('hidden'); | |
| } | |
| // Handle keyboard key presses | |
| document.addEventListener('keydown', (e) => { | |
| if (!notificationPopup.classList.contains('hidden')) { | |
| // If notification is open, only handle Escape key | |
| if (e.key === 'Escape' || e.key === 'Backspace') { | |
| hideNotification(); | |
| } | |
| return; | |
| } | |
| const maxItemsInColumn = itemsInColumn[currentColumnIndex].length; | |
| if (maxItemsInColumn === 0) return; | |
| switch (e.key) { | |
| case 'ArrowLeft': | |
| currentColumnIndex = Math.max(0, currentColumnIndex - 1); | |
| break; | |
| case 'ArrowRight': | |
| currentColumnIndex = Math.min(columns.length - 1, currentColumnIndex + 1); | |
| break; | |
| case 'ArrowUp': | |
| // Wrapping logic for up key | |
| currentItemIndex[currentColumnIndex] = (currentItemIndex[currentColumnIndex] - 1 + maxItemsInColumn) % maxItemsInColumn; | |
| break; | |
| case 'ArrowDown': | |
| // Wrapping logic for down key | |
| currentItemIndex[currentColumnIndex] = (currentItemIndex[currentColumnIndex] + 1) % maxItemsInColumn; | |
| break; | |
| case 'Enter': | |
| const focusedItem = itemsInColumn[currentColumnIndex][currentItemIndex[currentColumnIndex]]; | |
| if (focusedItem) { | |
| showNotification(focusedItem); | |
| } | |
| break; | |
| case 'Escape': | |
| case 'Backspace': | |
| // Handled above | |
| break; | |
| default: | |
| return; | |
| } | |
| e.preventDefault(); | |
| updateFocus(); | |
| }); | |
| // Handle item clicks for initial focus setting | |
| columns.forEach(column => { | |
| column.querySelectorAll('li').forEach(item => { | |
| item.addEventListener('click', () => { | |
| currentColumnIndex = parseInt(item.dataset.column); | |
| currentItemIndex[currentColumnIndex] = parseInt(item.dataset.index); | |
| updateFocus(); | |
| }); | |
| }); | |
| }); | |
| // Handle closing notification via button click | |
| closeNotificationBtn.addEventListener('click', hideNotification); | |
| // Set initial focus when the page loads | |
| updateFocus(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment