Spaces:
Running
Running
Build a responsive mobile application with the following specifications and features.
dd59da7 verified Download index.html from Maxwelljro/prototype-1: direct link, hf CLI and curl.
- Browser
- Download file 16.8 kB
-
https://huggingface.co/spaces/Maxwelljro/prototype-1/resolve/main/index.html
- Command line
-
hf download hf://spaces/Maxwelljro/prototype-1/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Maxwelljro/prototype-1/resolve/main/index.html
16.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>YouTube Thumbnail Saver</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #f0f0f0; | |
| } | |
| .neo-brutalism { | |
| border: 3px solid #000; | |
| box-shadow: 4px 4px 0px #000; | |
| } | |
| .neo-brutalism-sm { | |
| border: 2px solid #000; | |
| box-shadow: 2px 2px 0px #000; | |
| } | |
| .active-nav { | |
| background-color: #000; | |
| color: white; | |
| } | |
| .thumbnail-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); | |
| gap: 16px; | |
| } | |
| @media (max-width: 640px) { | |
| .thumbnail-grid { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen flex flex-col pb-20"> | |
| <!-- Create Page --> | |
| <div id="create-page" class="container mx-auto px-4 py-8"> | |
| <h1 class="text-3xl font-bold mb-6 text-center">Create Thumbnail</h1> | |
| <div class="mb-6"> | |
| <input | |
| type="text" | |
| id="url-input" | |
| placeholder="Paste YouTube video or playlist URL here..." | |
| class="w-full p-4 border-2 border-black rounded-lg neo-brutalism focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| > | |
| <p id="url-error" class="text-red-500 text-sm mt-2 hidden">Invalid YouTube URL</p> | |
| </div> | |
| <div class="flex justify-center mb-6"> | |
| <button | |
| id="find-btn" | |
| class="bg-blue-500 text-white px-6 py-3 rounded-lg neo-brutalism hover:bg-blue-600 transition-colors mr-4" | |
| > | |
| Find Thumbnail | |
| </button> | |
| <button | |
| id="clear-btn" | |
| class="bg-gray-200 p-3 rounded-lg neo-brutalism hover:bg-gray-300 transition-colors" | |
| > | |
| <i data-feather="trash-2"></i> | |
| </button> | |
| </div> | |
| <div id="thumbnail-preview" class="hidden text-center"> | |
| <div class="relative inline-block"> | |
| <img id="thumbnail-img" src="" alt="Thumbnail" class="mx-auto mb-4 rounded-lg neo-brutalism"> | |
| <span id="content-type" class="absolute top-2 right-2 bg-black text-white px-2 py-1 text-xs rounded"></span> | |
| </div> | |
| <button | |
| id="save-btn" | |
| class="bg-green-500 text-white px-6 py-3 rounded-lg neo-brutalism hover:bg-green-600 transition-colors" | |
| > | |
| Save to Collection | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Collections Page --> | |
| <div id="collections-page" class="container mx-auto px-4 py-8 hidden"> | |
| <h1 class="text-3xl font-bold mb-6 text-center">Your Collections</h1> | |
| <div class="mb-6 flex justify-between items-center"> | |
| <h2 class="text-2xl font-semibold">Folders</h2> | |
| <button | |
| id="add-folder-btn" | |
| class="bg-purple-500 text-white px-4 py-2 rounded-lg neo-brutalism hover:bg-purple-600 transition-colors" | |
| > | |
| <i data-feather="plus"></i> Add Folder | |
| </button> | |
| </div> | |
| <div id="folders-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8"> | |
| <!-- Folders will be dynamically added here --> | |
| </div> | |
| <div id="folder-content" class="hidden"> | |
| <div class="flex items-center mb-4"> | |
| <button id="back-to-folders" class="mr-4"> | |
| <i data-feather="arrow-left"></i> | |
| </button> | |
| <h2 id="folder-title" class="text-2xl font-semibold"></h2> | |
| </div> | |
| <div id="thumbnails-container" class="thumbnail-grid"> | |
| <!-- Thumbnails will be dynamically added here --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Bottom Navigation --> | |
| <nav class="fixed bottom-0 left-0 right-0 bg-white border-t-2 border-black neo-brutalism"> | |
| <div class="flex justify-around items-center h-16"> | |
| <button id="nav-create" class="flex-1 h-full flex items-center justify-center active-nav"> | |
| <i data-feather="plus"></i> | |
| </button> | |
| <button id="nav-collections" class="flex-1 h-full flex items-center justify-center"> | |
| <i data-feather="folder"></i> | |
| </button> | |
| </div> | |
| </nav> | |
| <!-- Notification --> | |
| <div id="notification" class="fixed top-4 right-4 bg-black text-white px-4 py-2 rounded-lg neo-brutalism hidden"> | |
| Notification message | |
| </div> | |
| <script> | |
| // Initialize Feather Icons | |
| feather.replace(); | |
| // DOM Elements | |
| const createPage = document.getElementById('create-page'); | |
| const collectionsPage = document.getElementById('collections-page'); | |
| const navCreate = document.getElementById('nav-create'); | |
| const navCollections = document.getElementById('nav-collections'); | |
| const urlInput = document.getElementById('url-input'); | |
| const findBtn = document.getElementById('find-btn'); | |
| const clearBtn = document.getElementById('clear-btn'); | |
| const thumbnailPreview = document.getElementById('thumbnail-preview'); | |
| const thumbnailImg = document.getElementById('thumbnail-img'); | |
| const contentType = document.getElementById('content-type'); | |
| const saveBtn = document.getElementById('save-btn'); | |
| const urlError = document.getElementById('url-error'); | |
| const notification = document.getElementById('notification'); | |
| const foldersContainer = document.getElementById('folders-container'); | |
| const folderContent = document.getElementById('folder-content'); | |
| const folderTitle = document.getElementById('folder-title'); | |
| const thumbnailsContainer = document.getElementById('thumbnails-container'); | |
| const backToFolders = document.getElementById('back-to-folders'); | |
| const addFolderBtn = document.getElementById('add-folder-btn'); | |
| // State | |
| let currentThumbnail = null; | |
| let currentType = null; | |
| let folders = JSON.parse(localStorage.getItem('folders')) || []; | |
| // Navigation | |
| function showPage(page) { | |
| if (page === 'create') { | |
| createPage.classList.remove('hidden'); | |
| collectionsPage.classList.add('hidden'); | |
| navCreate.classList.add('active-nav'); | |
| navCollections.classList.remove('active-nav'); | |
| } else { | |
| createPage.classList.add('hidden'); | |
| collectionsPage.classList.remove('hidden'); | |
| navCreate.classList.remove('active-nav'); | |
| navCollections.classList.add('active-nav'); | |
| renderFolders(); | |
| } | |
| } | |
| navCreate.addEventListener('click', () => showPage('create')); | |
| navCollections.addEventListener('click', () => showPage('collections')); | |
| // URL Validation | |
| function isValidYouTubeUrl(url) { | |
| const regExp = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/; | |
| return regExp.test(url); | |
| } | |
| // Extract Video ID | |
| function getYouTubeId(url) { | |
| const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; | |
| const match = url.match(regExp); | |
| return (match && match[2].length === 11) ? match[2] : null; | |
| } | |
| // Extract Playlist ID | |
| function getPlaylistId(url) { | |
| const regExp = /[?&]list=([^#&?]+)/; | |
| const match = url.match(regExp); | |
| return match ? match[1] : null; | |
| } | |
| // Get First Video ID from Playlist | |
| async function getFirstVideoIdFromPlaylist(playlistId) { | |
| // In a real app, you would use YouTube Data API | |
| // For this demo, we'll simulate with a placeholder | |
| return new Promise(resolve => { | |
| setTimeout(() => resolve('dQw4w9WgXcQ'), 1000); // Rickroll ID | |
| }); | |
| } | |
| // Find Thumbnail | |
| findBtn.addEventListener('click', async () => { | |
| const url = urlInput.value.trim(); | |
| if (!isValidYouTubeUrl(url)) { | |
| urlError.classList.remove('hidden'); | |
| return; | |
| } | |
| urlError.classList.add('hidden'); | |
| let videoId = getYouTubeId(url); | |
| currentType = 'Video'; | |
| if (!videoId) { | |
| const playlistId = getPlaylistId(url); | |
| if (playlistId) { | |
| currentType = 'Playlist'; | |
| videoId = await getFirstVideoIdFromPlaylist(playlistId); | |
| } | |
| } | |
| if (videoId) { | |
| thumbnailImg.src = `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`; | |
| contentType.textContent = currentType; | |
| thumbnailPreview.classList.remove('hidden'); | |
| currentThumbnail = { | |
| id: videoId, | |
| url: `https://www.youtube.com/watch?v=${videoId}`, | |
| type: currentType | |
| }; | |
| } | |
| }); | |
| // Clear Input and Preview | |
| clearBtn.addEventListener('click', () => { | |
| urlInput.value = ''; | |
| thumbnailPreview.classList.add('hidden'); | |
| urlError.classList.add('hidden'); | |
| currentThumbnail = null; | |
| }); | |
| // Save to Collection | |
| saveBtn.addEventListener('click', () => { | |
| if (!currentThumbnail) return; | |
| if (folders.length === 0) { | |
| showNotification('Create a folder first!'); | |
| return; | |
| } | |
| // For demo, we'll just save to the first folder | |
| const folder = folders[0]; | |
| const existing = folder.thumbnails.find(t => t.id === currentThumbnail.id); | |
| if (existing) { | |
| showNotification('Already saved in this folder!'); | |
| return; | |
| } | |
| folder.thumbnails.push(currentThumbnail); | |
| localStorage.setItem('folders', JSON.stringify(folders)); | |
| showNotification('Saved to collection!'); | |
| }); | |
| // Render Folders | |
| function renderFolders() { | |
| foldersContainer.innerHTML = ''; | |
| if (folders.length === 0) { | |
| foldersContainer.innerHTML = ` | |
| <div class="col-span-full text-center py-12"> | |
| <i data-feather="folder" class="text-gray-400 mb-4" style="width: 64px; height: 64px;"></i> | |
| <p class="text-gray-600">No folders yet. Create one to get started.</p> | |
| </div> | |
| `; | |
| feather.replace(); | |
| return; | |
| } | |
| folders.forEach((folder, index) => { | |
| const folderEl = document.createElement('div'); | |
| folderEl.className = 'bg-white p-4 rounded-lg neo-brutalism cursor-pointer hover:shadow-lg transition-shadow'; | |
| folderEl.innerHTML = ` | |
| <div class="flex justify-between items-start mb-2"> | |
| <h3 class="font-bold text-lg">${folder.name}</h3> | |
| <span class="bg-gray-200 px-2 py-1 rounded text-sm">${folder.thumbnails.length} items</span> | |
| </div> | |
| <p class="text-gray-600 text-sm mb-3">Click to view contents</p> | |
| <div class="flex justify-between"> | |
| <button class="edit-folder text-blue-500" data-index="${index}"> | |
| <i data-feather="edit-2"></i> | |
| </button> | |
| <button class="delete-folder text-red-500" data-index="${index}"> | |
| <i data-feather="trash-2"></i> | |
| </button> | |
| </div> | |
| `; | |
| foldersContainer.appendChild(folderEl); | |
| folderEl.addEventListener('click', (e) => { | |
| if (!e.target.closest('.edit-folder') && !e.target.closest('.delete-folder')) { | |
| showFolderContent(index); | |
| } | |
| }); | |
| folderEl.querySelector('.edit-folder').addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const newName = prompt('Enter new folder name:', folder.name); | |
| if (newName) { | |
| folder.name = newName; | |
| localStorage.setItem('folders', JSON.stringify(folders)); | |
| renderFolders(); | |
| } | |
| }); | |
| folderEl.querySelector('.delete-folder').addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| if (confirm(`Delete folder "${folder.name}" and all its contents?`)) { | |
| folders.splice(index, 1); | |
| localStorage.setItem('folders', JSON.stringify(folders)); | |
| renderFolders(); | |
| } | |
| }); | |
| }); | |
| feather.replace(); | |
| } | |
| // Show Folder Content | |
| function showFolderContent(folderIndex) { | |
| const folder = folders[folderIndex]; | |
| folderTitle.textContent = folder.name; | |
| thumbnailsContainer.innerHTML = ''; | |
| if (folder.thumbnails.length === 0) { | |
| thumbnailsContainer.innerHTML = ` | |
| <div class="col-span-full text-center py-8"> | |
| <p class="text-gray-600">This folder is empty.</p> | |
| </div> | |
| `; | |
| } else { | |
| folder.thumbnails.forEach(thumbnail => { | |
| const thumbEl = document.createElement('div'); | |
| thumbEl.className = 'relative cursor-pointer group'; | |
| thumbEl.innerHTML = ` | |
| <img src="https://img.youtube.com/vi/${thumbnail.id}/mqdefault.jpg" alt="Thumbnail" class="w-full rounded-lg neo-brutalism"> | |
| <div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all rounded-lg flex items-center justify-center"> | |
| <i data-feather="play-circle" class="text-white opacity-0 group-hover:opacity-100 transition-opacity"></i> | |
| </div> | |
| `; | |
| thumbEl.addEventListener('click', () => { | |
| window.open(thumbnail.url, '_blank'); | |
| }); | |
| thumbnailsContainer.appendChild(thumbEl); | |
| }); | |
| } | |
| folderContent.classList.remove('hidden'); | |
| foldersContainer.classList.add('hidden'); | |
| feather.replace(); | |
| } | |
| // Back to Folders | |
| backToFolders.addEventListener('click', () => { | |
| folderContent.classList.add('hidden'); | |
| foldersContainer.classList.remove('hidden'); | |
| }); | |
| // Add Folder | |
| addFolderBtn.addEventListener('click', () => { | |
| const name = prompt('Enter folder name:'); | |
| if (name) { | |
| folders.push({ | |
| name: name, | |
| thumbnails: [] | |
| }); | |
| localStorage.setItem('folders', JSON.stringify(folders)); | |
| renderFolders(); | |
| } | |
| }); | |
| // Show Notification | |
| function showNotification(message) { | |
| notification.textContent = message; | |
| notification.classList.remove('hidden'); | |
| setTimeout(() => { | |
| notification.classList.add('hidden'); | |
| }, 3000); | |
| } | |
| // Initialize | |
| showPage('create'); | |
| renderFolders(); | |
| </script> | |
| </body> | |
| </html> | |