Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Workout Video Archiver | Trainer's Toolkit</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* Custom styles that can't be done with Tailwind */ | |
| .video-upload-area { | |
| border: 2px dashed #cbd5e0; | |
| transition: all 0.3s ease; | |
| } | |
| .video-upload-area:hover { | |
| border-color: #4f46e5; | |
| background-color: #f8fafc; | |
| } | |
| .video-upload-area.dragover { | |
| border-color: #4f46e5; | |
| background-color: #e0e7ff; | |
| } | |
| .video-thumbnail { | |
| transition: transform 0.2s ease; | |
| } | |
| .video-thumbnail:hover { | |
| transform: scale(1.03); | |
| } | |
| .tag-cloud { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| .tag-item { | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .tag-item:hover { | |
| transform: translateY(-2px); | |
| } | |
| .sidebar { | |
| transition: all 0.3s ease; | |
| } | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| transform: translateX(-100%); | |
| position: fixed; | |
| z-index: 50; | |
| height: 100vh; | |
| top: 0; | |
| left: 0; | |
| } | |
| .sidebar.open { | |
| transform: translateX(0); | |
| } | |
| } | |
| .video-player-container { | |
| aspect-ratio: 16/9; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 font-sans"> | |
| <div class="flex h-screen overflow-hidden"> | |
| <!-- Sidebar --> | |
| <div class="sidebar bg-indigo-700 text-white w-64 flex-shrink-0 p-4 md:block"> | |
| <div class="flex items-center justify-between mb-8"> | |
| <h1 class="text-2xl font-bold">Trainer's Toolkit</h1> | |
| <button id="closeSidebar" class="md:hidden text-white"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <nav> | |
| <div class="mb-6"> | |
| <h2 class="text-sm uppercase font-semibold text-indigo-200 mb-2">Navigation</h2> | |
| <ul class="space-y-1"> | |
| <li> | |
| <button id="dashboardBtn" class="w-full text-left px-3 py-2 rounded-md bg-indigo-800 flex items-center"> | |
| <i class="fas fa-home mr-2"></i> Dashboard | |
| </button> | |
| </li> | |
| <li> | |
| <button id="uploadBtn" class="w-full text-left px-3 py-2 rounded-md hover:bg-indigo-600 flex items-center"> | |
| <i class="fas fa-upload mr-2"></i> Upload Videos | |
| </button> | |
| </li> | |
| <li> | |
| <button id="libraryBtn" class="w-full text-left px-3 py-2 rounded-md hover:bg-indigo-600 flex items-center"> | |
| <i class="fas fa-video mr-2"></i> Video Library | |
| </button> | |
| </li> | |
| <li> | |
| <button id="tagsBtn" class="w-full text-left px-3 py-2 rounded-md hover:bg-indigo-600 flex items-center"> | |
| <i class="fas fa-tags mr-2"></i> Manage Tags | |
| </button> | |
| </li> | |
| </ul> | |
| </div> | |
| <div class="mb-6"> | |
| <h2 class="text-sm uppercase font-semibold text-indigo-200 mb-2">Quick Filters</h2> | |
| <ul class="space-y-1"> | |
| <li> | |
| <button data-filter="recent" class="w-full text-left px-3 py-2 rounded-md hover:bg-indigo-600 flex items-center"> | |
| <i class="fas fa-clock mr-2"></i> Recent Videos | |
| </button> | |
| </li> | |
| <li> | |
| <button data-filter="week" class="w-full text-left px-3 py-2 rounded-md hover:bg-indigo-600 flex items-center"> | |
| <i class="fas fa-calendar-week mr-2"></i> This Week | |
| </button> | |
| </li> | |
| <li> | |
| <button data-filter="month" class="w-full text-left px-3 py-2 rounded-md hover:bg-indigo-600 flex items-center"> | |
| <i class="fas fa-calendar-alt mr-2"></i> This Month | |
| </button> | |
| </li> | |
| </ul> | |
| </div> | |
| <div> | |
| <h2 class="text-sm uppercase font-semibold text-indigo-200 mb-2">Popular Tags</h2> | |
| <div class="tag-cloud" id="popularTags"> | |
| <!-- Tags will be dynamically inserted here --> | |
| </div> | |
| </div> | |
| </nav> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="flex-1 flex flex-col overflow-hidden"> | |
| <!-- Top Navigation --> | |
| <header class="bg-white shadow-sm z-10"> | |
| <div class="flex items-center justify-between px-6 py-4"> | |
| <div class="flex items-center"> | |
| <button id="menuBtn" class="mr-4 text-gray-600 md:hidden"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| <h2 id="pageTitle" class="text-xl font-semibold text-gray-800">Dashboard</h2> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <div class="relative"> | |
| <input type="text" placeholder="Search videos..." class="pl-10 pr-4 py-2 border rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> | |
| </div> | |
| <div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-700 font-semibold"> | |
| T | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Page Content --> | |
| <main class="flex-1 overflow-y-auto p-6 bg-gray-50" id="mainContent"> | |
| <!-- Dashboard Content --> | |
| <div id="dashboardContent"> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6"> | |
| <div class="bg-white p-6 rounded-lg shadow"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-gray-500">Total Videos</p> | |
| <h3 class="text-2xl font-bold" id="totalVideos">0</h3> | |
| </div> | |
| <div class="p-3 rounded-full bg-indigo-100 text-indigo-600"> | |
| <i class="fas fa-video"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white p-6 rounded-lg shadow"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-gray-500">Total Tags</p> | |
| <h3 class="text-2xl font-bold" id="totalTags">0</h3> | |
| </div> | |
| <div class="p-3 rounded-full bg-green-100 text-green-600"> | |
| <i class="fas fa-tags"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white p-6 rounded-lg shadow"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-gray-500">Storage Used</p> | |
| <h3 class="text-2xl font-bold" id="storageUsed">0 MB</h3> | |
| </div> | |
| <div class="p-3 rounded-full bg-blue-100 text-blue-600"> | |
| <i class="fas fa-database"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white p-6 rounded-lg shadow mb-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-semibold">Recent Videos</h3> | |
| <button id="viewAllVideos" class="text-indigo-600 hover:text-indigo-800 text-sm font-medium">View All</button> | |
| </div> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4" id="recentVideos"> | |
| <!-- Recent videos will be inserted here --> | |
| </div> | |
| </div> | |
| <div class="bg-white p-6 rounded-lg shadow"> | |
| <h3 class="text-lg font-semibold mb-4">Quick Actions</h3> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| <button id="quickUploadBtn" class="flex flex-col items-center justify-center p-4 border rounded-lg hover:bg-gray-50 transition-colors"> | |
| <div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 mb-2"> | |
| <i class="fas fa-upload"></i> | |
| </div> | |
| <span class="text-sm font-medium">Upload Video</span> | |
| </button> | |
| <button id="quickTagBtn" class="flex flex-col items-center justify-center p-4 border rounded-lg hover:bg-gray-50 transition-colors"> | |
| <div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 mb-2"> | |
| <i class="fas fa-tags"></i> | |
| </div> | |
| <span class="text-sm font-medium">Manage Tags</span> | |
| </button> | |
| <button id="quickShareBtn" class="flex flex-col items-center justify-center p-4 border rounded-lg hover:bg-gray-50 transition-colors"> | |
| <div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 mb-2"> | |
| <i class="fas fa-share-alt"></i> | |
| </div> | |
| <span class="text-sm font-medium">Share Videos</span> | |
| </button> | |
| <button id="quickStatsBtn" class="flex flex-col items-center justify-center p-4 border rounded-lg hover:bg-gray-50 transition-colors"> | |
| <div class="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center text-purple-600 mb-2"> | |
| <i class="fas fa-chart-line"></i> | |
| </div> | |
| <span class="text-sm font-medium">View Stats</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Upload Content --> | |
| <div id="uploadContent" class="hidden"> | |
| <div class="bg-white p-6 rounded-lg shadow mb-6"> | |
| <h2 class="text-xl font-semibold mb-4">Upload Workout Videos</h2> | |
| <div class="video-upload-area rounded-lg p-8 text-center mb-6" id="dropArea"> | |
| <i class="fas fa-cloud-upload-alt text-4xl text-indigo-500 mb-3"></i> | |
| <h3 class="text-lg font-medium mb-1">Drag & Drop your workout videos here</h3> | |
| <p class="text-gray-500 mb-4">or</p> | |
| <label for="videoUpload" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 cursor-pointer"> | |
| <i class="fas fa-folder-open mr-2"></i> Browse Files | |
| <input type="file" id="videoUpload" class="hidden" accept="video/*" multiple> | |
| </label> | |
| </div> | |
| <div class="mb-6"> | |
| <h3 class="text-lg font-medium mb-3">Upload Queue</h3> | |
| <div class="border rounded-lg overflow-hidden"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">File Name</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Size</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Progress</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th> | |
| </tr> | |
| </thead> | |
| <tbody class="bg-white divide-y divide-gray-200" id="uploadQueue"> | |
| <!-- Upload queue will be inserted here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label for="workoutName" class="block text-sm font-medium text-gray-700 mb-1">Workout Name</label> | |
| <input type="text" id="workoutName" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="e.g. Upper Body Strength"> | |
| </div> | |
| <div> | |
| <label for="trainingName" class="block text-sm font-medium text-gray-700 mb-1">Training Name</label> | |
| <input type="text" id="trainingName" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="e.g. Shoulder Press Technique"> | |
| </div> | |
| </div> | |
| <div class="mt-6"> | |
| <label for="videoTags" class="block text-sm font-medium text-gray-700 mb-1">Tags (comma separated)</label> | |
| <input type="text" id="videoTags" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="e.g. strength, shoulders, technique"> | |
| </div> | |
| <div class="mt-6"> | |
| <label for="videoDescription" class="block text-sm font-medium text-gray-700 mb-1">Description</label> | |
| <textarea id="videoDescription" rows="3" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Add a description for this workout video..."></textarea> | |
| </div> | |
| <div class="mt-6 flex justify-end"> | |
| <button id="cancelUpload" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 mr-3">Cancel</button> | |
| <button id="startUpload" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Start Upload</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Video Library Content --> | |
| <div id="libraryContent" class="hidden"> | |
| <div class="bg-white p-6 rounded-lg shadow mb-6"> | |
| <div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6"> | |
| <h2 class="text-xl font-semibold mb-4 md:mb-0">Video Library</h2> | |
| <div class="flex flex-col sm:flex-row gap-3"> | |
| <div class="relative"> | |
| <select id="sortBy" class="appearance-none pl-3 pr-8 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white"> | |
| <option value="date-desc">Sort by: Newest First</option> | |
| <option value="date-asc">Sort by: Oldest First</option> | |
| <option value="name-asc">Sort by: Name (A-Z)</option> | |
| <option value="name-desc">Sort by: Name (Z-A)</option> | |
| </select> | |
| <i class="fas fa-chevron-down absolute right-3 top-3 text-gray-400 pointer-events-none"></i> | |
| </div> | |
| <div class="relative"> | |
| <select id="filterBy" class="appearance-none pl-3 pr-8 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white"> | |
| <option value="all">Filter by: All Videos</option> | |
| <option value="week">This Week</option> | |
| <option value="month">This Month</option> | |
| <option value="year">This Year</option> | |
| </select> | |
| <i class="fas fa-chevron-down absolute right-3 top-3 text-gray-400 pointer-events-none"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <div class="tag-cloud" id="activeFilters"> | |
| <!-- Active filters will be shown here --> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" id="videoGrid"> | |
| <!-- Videos will be inserted here --> | |
| </div> | |
| <div class="mt-6 flex justify-center" id="loadMoreContainer"> | |
| <button id="loadMoreVideos" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Load More Videos</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Tags Management Content --> | |
| <div id="tagsContent" class="hidden"> | |
| <div class="bg-white p-6 rounded-lg shadow mb-6"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-xl font-semibold">Manage Tags</h2> | |
| <button id="addNewTag" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 flex items-center"> | |
| <i class="fas fa-plus mr-2"></i> New Tag | |
| </button> | |
| </div> | |
| <div class="mb-6"> | |
| <div class="relative"> | |
| <input type="text" id="tagSearch" class="w-full pl-10 pr-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Search tags..."> | |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> | |
| </div> | |
| </div> | |
| <div class="border rounded-lg overflow-hidden"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tag Name</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Color</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Videos Count</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody class="bg-white divide-y divide-gray-200" id="tagsTable"> | |
| <!-- Tags will be inserted here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Video Detail Modal --> | |
| <div id="videoModal" class="fixed inset-0 z-50 hidden overflow-y-auto"> | |
| <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | |
| <div class="fixed inset-0 transition-opacity" aria-hidden="true"> | |
| <div class="absolute inset-0 bg-gray-500 opacity-75"></div> | |
| </div> | |
| <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> | |
| <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full"> | |
| <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | |
| <div class="flex justify-between items-start"> | |
| <h3 class="text-lg font-medium text-gray-900" id="modalVideoTitle">Video Title</h3> | |
| <button id="closeModal" class="text-gray-400 hover:text-gray-500"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="mt-4 video-player-container bg-black rounded-lg overflow-hidden"> | |
| <video controls class="w-full h-full" id="modalVideoPlayer"> | |
| Your browser does not support the video tag. | |
| </video> | |
| </div> | |
| <div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div class="md:col-span-2"> | |
| <div class="flex items-center mb-2"> | |
| <i class="fas fa-calendar-alt text-gray-500 mr-2"></i> | |
| <span class="text-sm text-gray-600" id="modalVideoDate">Uploaded on: </span> | |
| </div> | |
| <div class="flex items-center mb-2"> | |
| <i class="fas fa-tag text-gray-500 mr-2"></i> | |
| <div class="tag-cloud" id="modalVideoTags"> | |
| <!-- Tags will be inserted here --> | |
| </div> | |
| </div> | |
| <div class="flex items-center mb-2"> | |
| <i class="fas fa-dumbbell text-gray-500 mr-2"></i> | |
| <span class="text-sm text-gray-600" id="modalVideoWorkout">Workout: </span> | |
| </div> | |
| <div class="flex items-center mb-2"> | |
| <i class="fas fa-running text-gray-500 mr-2"></i> | |
| <span class="text-sm text-gray-600" id="modalVideoTraining">Training: </span> | |
| </div> | |
| <div class="mt-3"> | |
| <p class="text-sm text-gray-700" id="modalVideoDescription">No description provided.</p> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <h4 class="font-medium mb-3">Share Options</h4> | |
| <div class="space-y-3"> | |
| <button class="w-full flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md bg-white text-gray-700 hover:bg-gray-50"> | |
| <i class="fab fa-whatsapp text-green-500 mr-2"></i> Share via WhatsApp | |
| </button> | |
| <button class="w-full flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md bg-white text-gray-700 hover:bg-gray-50"> | |
| <i class="fas fa-envelope text-blue-500 mr-2"></i> Share via Email | |
| </button> | |
| <div class="mt-2"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Direct Link</label> | |
| <div class="flex"> | |
| <input type="text" id="videoLink" class="flex-1 px-3 py-2 border rounded-l-md focus:outline-none focus:ring-2 focus:ring-indigo-500" readonly> | |
| <button id="copyLink" class="px-3 py-2 bg-indigo-600 text-white rounded-r-md hover:bg-indigo-700"> | |
| <i class="fas fa-copy"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-4 pt-4 border-t border-gray-200"> | |
| <h4 class="font-medium mb-3">Video Actions</h4> | |
| <div class="space-y-2"> | |
| <button id="editVideoBtn" class="w-full flex items-center justify-start px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md"> | |
| <i class="fas fa-edit text-indigo-500 mr-2"></i> Edit Details | |
| </button> | |
| <button id="downloadVideoBtn" class="w-full flex items-center justify-start px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md"> | |
| <i class="fas fa-download text-green-500 mr-2"></i> Download Video | |
| </button> | |
| <button id="deleteVideoBtn" class="w-full flex items-center justify-start px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md"> | |
| <i class="fas fa-trash-alt text-red-500 mr-2"></i> Delete Video | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Edit Video Modal --> | |
| <div id="editVideoModal" class="fixed inset-0 z-50 hidden overflow-y-auto"> | |
| <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | |
| <div class="fixed inset-0 transition-opacity" aria-hidden="true"> | |
| <div class="absolute inset-0 bg-gray-500 opacity-75"></div> | |
| </div> | |
| <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> | |
| <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full"> | |
| <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | |
| <div class="flex justify-between items-start"> | |
| <h3 class="text-lg font-medium text-gray-900">Edit Video Details</h3> | |
| <button id="closeEditModal" class="text-gray-400 hover:text-gray-500"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="mt-4"> | |
| <input type="hidden" id="editVideoId"> | |
| <div class="mb-4"> | |
| <label for="editVideoTitle" class="block text-sm font-medium text-gray-700 mb-1">Video Title</label> | |
| <input type="text" id="editVideoTitle" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <label for="editWorkoutName" class="block text-sm font-medium text-gray-700 mb-1">Workout Name</label> | |
| <input type="text" id="editWorkoutName" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| </div> | |
| <div> | |
| <label for="editTrainingName" class="block text-sm font-medium text-gray-700 mb-1">Training Name</label> | |
| <input type="text" id="editTrainingName" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <label for="editVideoTags" class="block text-sm font-medium text-gray-700 mb-1">Tags (comma separated)</label> | |
| <input type="text" id="editVideoTags" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| </div> | |
| <div> | |
| <label for="editVideoDescription" class="block text-sm font-medium text-gray-700 mb-1">Description</label> | |
| <textarea id="editVideoDescription" rows="3" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> | |
| <button id="saveVideoChanges" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm"> | |
| Save Changes | |
| </button> | |
| <button id="cancelVideoEdit" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"> | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Add Tag Modal --> | |
| <div id="addTagModal" class="fixed inset-0 z-50 hidden overflow-y-auto"> | |
| <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | |
| <div class="fixed inset-0 transition-opacity" aria-hidden="true"> | |
| <div class="absolute inset-0 bg-gray-500 opacity-75"></div> | |
| </div> | |
| <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> | |
| <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-md sm:w-full"> | |
| <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | |
| <div class="flex justify-between items-start"> | |
| <h3 class="text-lg font-medium text-gray-900">Add New Tag</h3> | |
| <button id="closeTagModal" class="text-gray-400 hover:text-gray-500"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="mt-4"> | |
| <div class="mb-4"> | |
| <label for="newTagName" class="block text-sm font-medium text-gray-700 mb-1">Tag Name</label> | |
| <input type="text" id="newTagName" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Tag Color</label> | |
| <div class="flex flex-wrap gap-2"> | |
| <div class="w-8 h-8 rounded-full bg-red-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="red"></div> | |
| <div class="w-8 h-8 rounded-full bg-blue-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="blue"></div> | |
| <div class="w-8 h-8 rounded-full bg-green-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="green"></div> | |
| <div class="w-8 h-8 rounded-full bg-yellow-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="yellow"></div> | |
| <div class="w-8 h-8 rounded-full bg-indigo-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="indigo"></div> | |
| <div class="w-8 h-8 rounded-full bg-purple-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="purple"></div> | |
| <div class="w-8 h-8 rounded-full bg-pink-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="pink"></div> | |
| <div class="w-8 h-8 rounded-full bg-gray-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="gray"></div> | |
| </div> | |
| <input type="hidden" id="selectedTagColor" value="indigo"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> | |
| <button id="saveNewTag" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm"> | |
| Add Tag | |
| </button> | |
| <button id="cancelNewTag" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"> | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Confirmation Modal --> | |
| <div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto"> | |
| <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | |
| <div class="fixed inset-0 transition-opacity" aria-hidden="true"> | |
| <div class="absolute inset-0 bg-gray-500 opacity-75"></div> | |
| </div> | |
| <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> | |
| <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> | |
| <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | |
| <div class="sm:flex sm:items-start"> | |
| <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> | |
| <i class="fas fa-exclamation-triangle text-red-600"></i> | |
| </div> | |
| <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> | |
| <h3 class="text-lg leading-6 font-medium text-gray-900" id="confirmModalTitle">Confirm Action</h3> | |
| <div class="mt-2"> | |
| <p class="text-sm text-gray-500" id="confirmModalMessage">Are you sure you want to perform this action?</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> | |
| <button id="confirmAction" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"> | |
| Confirm | |
| </button> | |
| <button id="cancelAction" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"> | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Notification Toast --> | |
| <div id="toast" class="fixed bottom-4 right-4 hidden"> | |
| <div class="bg-green-500 text-white px-4 py-2 rounded-md shadow-lg flex items-center"> | |
| <i class="fas fa-check-circle mr-2"></i> | |
| <span id="toastMessage">Operation completed successfully!</span> | |
| <button id="closeToast" class="ml-4"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| <script> | |
| // Sample data storage (in a real app, this would be a database) | |
| let videos = []; | |
| let tags = [ | |
| { id: 1, name: 'strength', color: 'red', count: 0 }, | |
| { id: 2, name: 'cardio', color: 'blue', count: 0 }, | |
| { id: 3, name: 'flexibility', color: 'green', count: 0 }, | |
| { id: 4, name: 'technique', color: 'yellow', count: 0 }, | |
| { id: 5, name: 'beginner', color: 'indigo', count: 0 }, | |
| { id: 6, name: 'advanced', color: 'purple', count: 0 } | |
| ]; | |
| // DOM Elements | |
| const menuBtn = document.getElementById('menuBtn'); | |
| const closeSidebar = document.getElementById('closeSidebar'); | |
| const sidebar = document.querySelector('.sidebar'); | |
| const pageTitle = document.getElementById('pageTitle'); | |
| const mainContent = document.getElementById('mainContent'); | |
| // Content sections | |
| const dashboardContent = document.getElementById('dashboardContent'); | |
| const uploadContent = document.getElementById('uploadContent'); | |
| const libraryContent = document.getElementById('libraryContent'); | |
| const tagsContent = document.getElementById('tagsContent'); | |
| // Navigation buttons | |
| const dashboardBtn = document.getElementById('dashboardBtn'); | |
| const uploadBtn = document.getElementById('uploadBtn'); | |
| const libraryBtn = document.getElementById('libraryBtn'); | |
| const tagsBtn = document.getElementById('tagsBtn'); | |
| // Quick action buttons | |
| const quickUploadBtn = document.getElementById('quickUploadBtn'); | |
| const quickTagBtn = document.getElementById('quickTagBtn'); | |
| const quickShareBtn = document.getElementById('quickShareBtn'); | |
| const quickStatsBtn = document.getElementById('quickStatsBtn'); | |
| const viewAllVideos = document.getElementById('viewAllVideos'); | |
| // Upload section elements | |
| const dropArea = document.getElementById('dropArea'); | |
| const videoUpload = document.getElementById('videoUpload'); | |
| const uploadQueue = document.getElementById('uploadQueue'); | |
| const startUpload = document.getElementById('startUpload'); | |
| const cancelUpload = document.getElementById('cancelUpload'); | |
| // Library section elements | |
| const videoGrid = document.getElementById('videoGrid'); | |
| const sortBy = document.getElementById('sortBy'); | |
| const filterBy = document.getElementById('filterBy'); | |
| const activeFilters = document.getElementById('activeFilters'); | |
| const loadMoreVideos = document.getElementById('loadMoreVideos'); | |
| const loadMoreContainer = document.getElementById('loadMoreContainer'); | |
| const popularTags = document.getElementById('popularTags'); | |
| // Tags section elements | |
| const tagsTable = document.getElementById('tagsTable'); | |
| const addNewTag = document.getElementById('addNewTag'); | |
| const tagSearch = document.getElementById('tagSearch'); | |
| // Modal elements | |
| const videoModal = document.getElementById('videoModal'); | |
| const modalVideoTitle = document.getElementById('modalVideoTitle'); | |
| const modalVideoPlayer = document.getElementById('modalVideoPlayer'); | |
| const modalVideoDate = document.getElementById('modalVideoDate'); | |
| const modalVideoTags = document.getElementById('modalVideoTags'); | |
| const modalVideoWorkout = document.getElementById('modalVideoWorkout'); | |
| const modalVideoTraining = document.getElementById('modalVideoTraining'); | |
| const modalVideoDescription = document.getElementById('modalVideoDescription'); | |
| const videoLink = document.getElementById('videoLink'); | |
| const copyLink = document.getElementById('copyLink'); | |
| const closeModal = document.getElementById('closeModal'); | |
| const editVideoModal = document.getElementById('editVideoModal'); | |
| const editVideoId = document.getElementById('editVideoId'); | |
| const editVideoTitle = document.getElementById('editVideoTitle'); | |
| const editWorkoutName = document.getElementById('editWorkoutName'); | |
| const editTrainingName = document.getElementById('editTrainingName'); | |
| const editVideoTags = document.getElementById('editVideoTags'); | |
| const editVideoDescription = document.getElementById('editVideoDescription'); | |
| const saveVideoChanges = document.getElementById('saveVideoChanges'); | |
| const cancelVideoEdit = document.getElementById('cancelVideoEdit'); | |
| const closeEditModal = document.getElementById('closeEditModal'); | |
| const addTagModal = document.getElementById('addTagModal'); | |
| const newTagName = document.getElementById('newTagName'); | |
| const selectedTagColor = document.getElementById('selectedTagColor'); | |
| const saveNewTag = document.getElementById('saveNewTag'); | |
| const cancelNewTag = document.getElementById('cancelNewTag'); | |
| const closeTagModal = document.getElementById('closeTagModal'); | |
| const confirmModal = document.getElementById('confirmModal'); | |
| const confirmModalTitle = document.getElementById('confirmModalTitle'); | |
| const confirmModalMessage = document.getElementById('confirmModalMessage'); | |
| const confirmAction = document.getElementById('confirmAction'); | |
| const cancelAction = document.getElementById('cancelAction'); | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toastMessage'); | |
| const closeToast = document.getElementById('closeToast'); | |
| // App state | |
| let currentView = 'dashboard'; | |
| let currentVideoPage = 1; | |
| let videosPerPage = 8; | |
| let selectedVideoId = null; | |
| let filesToUpload = []; | |
| let activeTagFilters = []; | |
| let actionToConfirm = null; | |
| // Initialize the app | |
| function init() { | |
| // Load sample data | |
| loadSampleVideos(); | |
| updateTagCounts(); | |
| // Set up event listeners | |
| setupEventListeners(); | |
| // Render initial views | |
| renderDashboard(); | |
| renderPopularTags(); | |
| } | |
| // Load sample videos for demo purposes | |
| function loadSampleVideos() { | |
| const sampleVideos = [ | |
| { | |
| id: 1, | |
| title: 'Shoulder Press Technique', | |
| workout: 'Upper Body Strength', | |
| training: 'Shoulder Press', | |
| description: 'Detailed explanation of proper shoulder press technique to maximize results and prevent injury.', | |
| tags: ['strength', 'shoulders', 'technique'], | |
| date: new Date('2023-06-15'), | |
| size: 45, | |
| thumbnail: 'https://img.freepik.com/free-photo/fit-man-training-gym_651396-1025.jpg', | |
| videoUrl: 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' | |
| }, | |
| { | |
| id: 2, | |
| title: 'Squat Depth and Form', | |
| workout: 'Lower Body Strength', | |
| training: 'Barbell Squat', | |
| description: 'How to achieve proper squat depth while maintaining good form throughout the movement.', | |
| tags: ['strength', 'legs', 'technique', 'beginner'], | |
| date: new Date('2023-06-10'), | |
| size: 62, | |
| thumbnail: 'https://img.freepik.com/free-photo/young-healthy-man-athlete-doing-exercise-with-ropes-gym-single-male-model-performing-hard-training-his-upper-body-concept-healthy-lifestyle-sport-fitness-bodybuilding-wellbeing_155003-27804.jpg', | |
| videoUrl: 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' | |
| }, | |
| { | |
| id: 3, | |
| title: 'HIIT Circuit Demo', | |
| workout: 'Cardio Blast', | |
| training: 'HIIT Routine', | |
| description: 'Full demonstration of our 20-minute high intensity interval training circuit.', | |
| tags: ['cardio', 'hiit', 'full-body'], | |
| date: new Date('2023-06-05'), | |
| size: 85, | |
| thumbnail: 'https://img.freepik.com/free-photo/young-fitness-man-studio_7502-5008.jpg', | |
| videoUrl: 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' | |
| }, | |
| { | |
| id: 4, | |
| title: 'Deadlift Variations', | |
| workout: 'Power Lifting', | |
| training: 'Deadlift', | |
| description: 'Comparison of conventional, sumo, and Romanian deadlift variations with pros and cons of each.', | |
| tags: ['strength', 'deadlift', 'advanced'], | |
| date: new Date('2023-05-28'), | |
| size: 78, | |
| thumbnail: 'https://img.freepik.com/free-photo/young-fitness-man-studio_7502-5006.jpg', | |
| videoUrl: 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' | |
| }, | |
| { | |
| id: 5, | |
| title: 'Dynamic Warmup Routine', | |
| workout: 'Pre-Workout', | |
| training: 'Warmup', | |
| description: '10-minute dynamic warmup routine to prepare your body for intense training sessions.', | |
| tags: ['warmup', 'flexibility', 'beginner'], | |
| date: new Date('2023-05-20'), | |
| size: 35, | |
| thumbnail: 'https://img.freepik.com/free-photo/young-sports-man-training-gym_1303-14701.jpg', | |
| videoUrl: 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' | |
| }, | |
| { | |
| id: 6, | |
| title: 'Advanced Pushup Variations', | |
| workout: 'Bodyweight Training', | |
| training: 'Pushups', | |
| description: '10 challenging pushup variations to take your upper body training to the next level.', | |
| tags: ['strength', 'bodyweight', 'advanced'], | |
| date: new Date('2023-05-15'), | |
| size: 52, | |
| thumbnail: 'https://img.freepik.com/free-photo/young-sports-man-training-gym_1303-14696.jpg', | |
| videoUrl: 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' | |
| }, | |
| { | |
| id: 7, | |
| title: 'Foam Rolling Techniques', | |
| workout: 'Recovery', | |
| training: 'Mobility', | |
| description: 'How to properly use a foam roller to improve recovery and mobility after workouts.', | |
| tags: ['recovery', 'flexibility', 'beginner'], | |
| date: new Date('2023-05-10'), | |
| size: 48, | |
| thumbnail: 'https://img.freepik.com/free-photo/young-sports-man-training-gym_1303-14700.jpg', | |
| videoUrl: 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' | |
| }, | |
| { | |
| id: 8, | |
| title: 'Kettlebell Swing Form', | |
| workout: 'Functional Training', | |
| training: 'Kettlebell', | |
| description: 'Step-by-step guide to mastering the kettlebell swing for power and endurance.', | |
| tags: ['strength', 'kettlebell', 'technique'], | |
| date: new Date('2023-05-05'), | |
| size: 55, | |
| thumbnail: 'https://img.freepik.com/free-photo/young-sports-man-training-gym_1303-14699.jpg', | |
| videoUrl: 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' | |
| } | |
| ]; | |
| videos = sampleVideos; | |
| } | |
| // Update tag counts based on videos | |
| function updateTagCounts() { | |
| tags.forEach(tag => { | |
| tag.count = videos.filter(video => video.tags.includes(tag.name)).length; | |
| }); | |
| } | |
| // Set up all event listeners | |
| function setupEventListeners() { | |
| // Navigation | |
| menuBtn.addEventListener('click', toggleSidebar); | |
| closeSidebar.addEventListener('click', toggleSidebar); | |
| dashboardBtn.addEventListener('click', () => switchView('dashboard')); | |
| uploadBtn.addEventListener('click', () => switchView('upload')); | |
| libraryBtn.addEventListener('click', () => switchView('library')); | |
| tagsBtn.addEventListener('click', () => switchView('tags')); | |
| // Quick actions | |
| quickUploadBtn.addEventListener('click', () => switchView('upload')); | |
| quickTagBtn.addEventListener('click', () => switchView('tags')); | |
| quickShareBtn.addEventListener('click', () => showToast('Share feature coming soon!')); | |
| quickStatsBtn.addEventListener('click', () => showToast('Statistics feature coming soon!')); | |
| viewAllVideos.addEventListener('click', () => switchView('library')); | |
| // Upload section | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropArea.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropArea.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropArea.addEventListener(eventName, unhighlight, false); | |
| }); | |
| dropArea.addEventListener('drop', handleDrop, false); | |
| videoUpload.addEventListener('change', handleFiles, false); | |
| startUpload.addEventListener('click', uploadFiles); | |
| cancelUpload.addEventListener('click', clearUploadQueue); | |
| // Library section | |
| sortBy.addEventListener('change', renderVideoLibrary); | |
| filterBy.addEventListener('change', renderVideoLibrary); | |
| loadMoreVideos.addEventListener('click', loadMoreVideosHandler); | |
| // Tags section | |
| addNewTag.addEventListener('click', showAddTagModal); | |
| tagSearch.addEventListener('input', filterTags); | |
| // Video modal | |
| closeModal.addEventListener('click', () => videoModal.classList.add('hidden')); | |
| copyLink.addEventListener('click', copyVideoLink); | |
| editVideoBtn.addEventListener('click', showEditVideoModal); | |
| downloadVideoBtn.addEventListener('click', downloadVideo); | |
| deleteVideoBtn.addEventListener('click', confirmDeleteVideo); | |
| // Edit video modal | |
| closeEditModal.addEventListener('click', () => editVideoModal.classList.add('hidden')); | |
| cancelVideoEdit.addEventListener('click', () => editVideoModal.classList.add('hidden')); | |
| saveVideoChanges.addEventListener('click', saveVideoEdits); | |
| // Add tag modal | |
| closeTagModal.addEventListener('click', () => addTagModal.classList.add('hidden')); | |
| cancelNewTag.addEventListener('click', () => addTagModal.classList.add('hidden')); | |
| saveNewTag.addEventListener('click', addNewTagHandler); | |
| // Color selection in add tag modal | |
| document.querySelectorAll('[data-color]').forEach(el => { | |
| el.addEventListener('click', function() { | |
| document.querySelectorAll('[data-color]').forEach(item => { | |
| item.classList.remove('border-gray-300'); | |
| item.classList.add('border-transparent'); | |
| }); | |
| this.classList.remove('border-transparent'); | |
| this.classList.add('border-gray-300'); | |
| selectedTagColor.value = this.getAttribute('data-color'); | |
| }); | |
| }); | |
| // Confirmation modal | |
| closeEditModal.addEventListener('click', () => confirmModal.classList.add('hidden')); | |
| cancelAction.addEventListener('click', () => confirmModal.classList.add('hidden')); | |
| confirmAction.addEventListener('click', performConfirmedAction); | |
| // Toast notification | |
| closeToast.addEventListener('click', () => toast.classList.add('hidden')); | |
| // Filter buttons | |
| document.querySelectorAll('[data-filter]').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| const filter = this.getAttribute('data-filter'); | |
| applyQuickFilter(filter); | |
| }); | |
| }); | |
| } | |
| // Toggle sidebar on mobile | |
| function toggleSidebar() { | |
| sidebar.classList.toggle('open'); | |
| } | |
| // Switch between different views | |
| function switchView(view) { | |
| currentView = view; | |
| // Hide all content sections | |
| dashboardContent.classList.add('hidden'); | |
| uploadContent.classList.add('hidden'); | |
| libraryContent.classList.add('hidden'); | |
| tagsContent.classList.add('hidden'); | |
| // Reset active states on nav buttons | |
| dashboardBtn.classList.remove('bg-indigo-800'); | |
| uploadBtn.classList.remove('bg-indigo-800'); | |
| libraryBtn.classList.remove('bg-indigo-800'); | |
| tagsBtn.classList.remove('bg-indigo-800'); | |
| // Show the selected content section | |
| switch(view) { | |
| case 'dashboard': | |
| dashboardContent.classList.remove('hidden'); | |
| dashboardBtn.classList.add('bg-indigo-800'); | |
| pageTitle.textContent = 'Dashboard'; | |
| renderDashboard(); | |
| break; | |
| case 'upload': | |
| uploadContent.classList.remove('hidden'); | |
| uploadBtn.classList.add('bg-indigo-800'); | |
| pageTitle.textContent = 'Upload Videos'; | |
| break; | |
| case 'library': | |
| libraryContent.classList.remove('hidden'); | |
| libraryBtn.classList.add('bg-indigo-800'); | |
| pageTitle.textContent = 'Video Library'; | |
| currentVideoPage = 1; | |
| renderVideoLibrary(); | |
| break; | |
| case 'tags': | |
| tagsContent.classList.remove('hidden'); | |
| tagsBtn.classList.add('bg-indigo-800'); | |
| pageTitle.textContent = 'Manage Tags'; | |
| renderTagsTable(); | |
| break; | |
| } | |
| // Close sidebar on mobile after selection | |
| if (window.innerWidth < 768) { | |
| sidebar.classList.remove('open'); | |
| } | |
| } | |
| // Render dashboard content | |
| function renderDashboard() { | |
| // Update stats | |
| document.getElementById('totalVideos').textContent = videos.length; | |
| document.getElementById('totalTags').textContent = tags.length; | |
| const totalSize = videos.reduce((sum, video) => sum + video.size, 0); | |
| document.getElementById('storageUsed').textContent = `${totalSize} MB`; | |
| // Render recent videos | |
| const recentVideosContainer = document.getElementById('recentVideos'); | |
| recentVideosContainer.innerHTML = ''; | |
| // Sort by date (newest first) and take first 3 | |
| const recentVideos = [...videos].sort((a, b) => b.date - a.date).slice(0, 3); | |
| recentVideos.forEach(video => { | |
| const videoElement = createVideoCard(video, true); | |
| recentVideosContainer.appendChild(videoElement); | |
| }); | |
| } | |
| // Render video library | |
| function renderVideoLibrary() { | |
| videoGrid.innerHTML = ''; | |
| currentVideoPage = 1; | |
| let filteredVideos = [...videos]; | |
| // Apply tag filters if any | |
| if (activeTagFilters.length > 0) { | |
| filteredVideos = filteredVideos.filter(video => | |
| activeTagFilters.some(tag => video.tags.includes(tag)) | |
| ); | |
| } | |
| // Apply time filter | |
| const timeFilter = filterBy.value; | |
| if (timeFilter !== 'all') { | |
| const now = new Date(); | |
| let startDate; | |
| switch(timeFilter) { | |
| case 'week': | |
| startDate = new Date(now.setDate(now.getDate() - 7)); | |
| break; | |
| case 'month': | |
| startDate = new Date(now.setMonth(now.getMonth() - 1)); | |
| break; | |
| case 'year': | |
| startDate = new Date(now.setFullYear(now.getFullYear() - 1)); | |
| break; | |
| } | |
| filteredVideos = filteredVideos.filter(video => video.date >= startDate); | |
| } | |
| // Apply sorting | |
| const sortOption = sortBy.value; | |
| switch(sortOption) { | |
| case 'date-desc': | |
| filteredVideos.sort((a, b) => b.date - a.date); | |
| break; | |
| case 'date-asc': | |
| filteredVideos.sort((a, b) => a.date - b.date); | |
| break; | |
| case 'name-asc': | |
| filteredVideos.sort((a, b) => a.title.localeCompare(b.title)); | |
| break; | |
| case 'name-desc': | |
| filteredVideos.sort((a, b) => b.title.localeCompare(a.title)); | |
| break; | |
| } | |
| // Display active filters | |
| renderActiveFilters(); | |
| // Check if there are videos to display | |
| if (filteredVideos.length === 0) { | |
| videoGrid.innerHTML = ` | |
| <div class="col-span-full text-center py-10"> | |
| <i class="fas fa-video-slash text-4xl text-gray-400 mb-3"></i> | |
| <h3 class="text-lg font-medium text-gray-700">No videos found</h3> | |
| <p class="text-gray-500">Try adjusting your filters or upload new videos</p> | |
| </div> | |
| `; | |
| loadMoreContainer.classList.add('hidden'); | |
| return; | |
| } | |
| // Display first page of videos | |
| displayVideosPaginated(filteredVideos); | |
| // Show/hide load more button | |
| if (filteredVideos.length > videosPerPage * currentVideoPage) { | |
| loadMoreContainer.classList.remove('hidden'); | |
| } else { | |
| loadMoreContainer.classList.add('hidden'); | |
| } | |
| } | |
| // Display videos with pagination | |
| function displayVideosPaginated(videoList) { | |
| const startIndex = 0; | |
| const endIndex = Math.min(videoList.length, videosPerPage * currentVideoPage); | |
| for (let i = startIndex; i < endIndex; i++) { | |
| const video = videoList[i]; | |
| const videoElement = createVideoCard(video); | |
| videoGrid.appendChild(videoElement); | |
| } | |
| } | |
| // Load more videos handler | |
| function loadMoreVideosHandler() { | |
| currentVideoPage++; | |
| renderVideoLibrary(); | |
| // Scroll to the bottom of the video grid | |
| setTimeout(() => { | |
| videoGrid.scrollIntoView({ behavior: 'smooth', block: 'end' }); | |
| }, 100); | |
| } | |
| // Create a video card element | |
| function createVideoCard(video, isSmall = false) { | |
| const videoElement = document.createElement('div'); | |
| videoElement.className = 'bg-white rounded-lg overflow-hidden shadow-md video-thumbnail'; | |
| videoElement.dataset.id = video.id; | |
| // Format date | |
| const formattedDate = video.date.toLocaleDateString('en-US', { | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric' | |
| }); | |
| if (isSmall) { | |
| videoElement.innerHTML = ` | |
| <div class="cursor-pointer" onclick="openVideoModal(${video.id})"> | |
| <div class="relative pb-[56.25%] bg-gray-200"> | |
| <img src="${video.thumbnail}" alt="${video.title}" class="absolute h-full w-full object-cover"> | |
| <div class="absolute bottom-2 right-2 bg-black bg-opacity-70 text-white text-xs px-2 py-1 rounded"> | |
| ${video.size} MB | |
| </div> | |
| </div> | |
| <div class="p-3"> | |
| <h3 class="font-medium text-gray-800 truncate">${video.title}</h3> | |
| <p class="text-xs text-gray-500 mt-1">${formattedDate}</p> | |
| <div class="flex flex-wrap gap-1 mt-2"> | |
| ${video.tags.map(tag => createTagElement(tag)).join('')} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } else { | |
| videoElement.innerHTML = ` | |
| <div class="cursor-pointer" onclick="openVideoModal(${video.id})"> | |
| <div class="relative pb-[56.25%] bg-gray-200"> | |
| <img src="${video.thumbnail}" alt="${video.title}" class="absolute h-full w-full object-cover"> | |
| <div class="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 bg-black bg-opacity-30 transition-opacity"> | |
| <div class="bg-white bg-opacity-80 rounded-full p-3"> | |
| <i class="fas fa-play text-indigo-600"></i> | |
| </div> | |
| </div> | |
| <div class="absolute bottom-2 right-2 bg-black bg-opacity-70 text-white text-xs px-2 py-1 rounded"> | |
| ${video.size} MB | |
| </div> | |
| </div> | |
| <div class="p-4"> | |
| <h3 class="font-medium text-gray-800 truncate">${video.title}</h3> | |
| <p class="text-sm text-gray-500 mt-1">${video.workout} • ${video.training}</p> | |
| <p class="text-xs text-gray-500 mt-1">${formattedDate}</p> | |
| <div class="flex flex-wrap gap-1 mt-2"> | |
| ${video.tags.map(tag => createTagElement(tag)).join('')} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| return videoElement; | |
| } | |
| // Create a tag element | |
| function createTagElement(tagName, isClickable = true) { | |
| const tag = tags.find(t => t.name === tagName); | |
| const color = tag ? tag.color : 'gray'; | |
| if (isClickable) { | |
| return `<span class="tag-item inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-${color}-100 text-${color}-800" onclick="event.stopPropagation(); filterByTag('${tagName}')">${tagName}</span>`; | |
| } else { | |
| return `<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-${color}-100 text-${color}-800">${tagName}</span>`; | |
| } | |
| } | |
| // Render active filters | |
| function renderActiveFilters() { | |
| activeFilters.innerHTML = ''; | |
| if (activeTagFilters.length > 0) { | |
| const filterTitle = document.createElement('span'); | |
| filterTitle.className = 'text-sm text-gray-500 mr-2'; | |
| filterTitle.textContent = 'Active filters:'; | |
| activeFilters.appendChild(filterTitle); | |
| activeTagFilters.forEach(tag => { | |
| const tagElement = document.createElement('span'); | |
| tagElement.className = 'tag-item inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800'; | |
| tagElement.innerHTML = `${tag} <button class="ml-1 text-indigo-500 hover:text-indigo-700" onclick="removeTagFilter('${tag}')"><i class="fas fa-times"></i></button>`; | |
| activeFilters.appendChild(tagElement); | |
| }); | |
| if (activeTagFilters.length > 0) { | |
| const clearAll = document.createElement('button'); | |
| clearAll.className = 'text-sm text-indigo-600 hover:text-indigo-800 ml-2'; | |
| clearAll.textContent = 'Clear all'; | |
| clearAll.onclick = clearAllFilters; | |
| activeFilters.appendChild(clearAll); | |
| } | |
| } | |
| } | |
| // Filter by tag | |
| function filterByTag(tagName) { | |
| if (!activeTagFilters.includes(tagName)) { | |
| activeTagFilters.push(tagName); | |
| renderVideoLibrary(); | |
| } | |
| } | |
| // Remove tag filter | |
| function removeTagFilter(tagName) { | |
| activeTagFilters = activeTagFilters.filter(tag => tag !== tagName); | |
| renderVideoLibrary(); | |
| } | |
| // Clear all filters | |
| function clearAllFilters() { | |
| activeTagFilters = []; | |
| filterBy.value = 'all'; | |
| renderVideoLibrary(); | |
| } | |
| // Apply quick filter from sidebar | |
| function applyQuickFilter(filter) { | |
| activeTagFilters = []; | |
| switch(filter) { | |
| case 'recent': | |
| sortBy.value = 'date-desc'; | |
| filterBy.value = 'all'; | |
| break; | |
| case 'week': | |
| filterBy.value = 'week'; | |
| break; | |
| case 'month': | |
| filterBy.value = 'month'; | |
| break; | |
| } | |
| switchView('library'); | |
| } | |
| // Render popular tags in sidebar | |
| function renderPopularTags() { | |
| popularTags.innerHTML = ''; | |
| // Sort tags by count (descending) and take top 6 | |
| const popular = [...tags].sort((a, b) => b.count - a.count).slice(0, 6); | |
| popular.forEach(tag => { | |
| const tagElement = document.createElement('span'); | |
| tagElement.className = `tag-item inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-${tag.color}-100 text-${tag.color}-800`; | |
| tagElement.textContent = tag.name; | |
| tagElement.onclick = function() { | |
| switchView('library'); | |
| filterByTag(tag.name); | |
| }; | |
| popularTags.appendChild(tagElement); | |
| }); | |
| } | |
| // Render tags table | |
| function renderTagsTable() { | |
| tagsTable.innerHTML = ''; | |
| if (tags.length === 0) { | |
| tagsTable.innerHTML = ` | |
| <tr> | |
| <td colspan="4" class="px-6 py-4 text-center text-gray-500"> | |
| No tags found. Create your first tag to get started. | |
| </td> | |
| </tr> | |
| `; | |
| return; | |
| } | |
| tags.forEach(tag => { | |
| const row = document.createElement('tr'); | |
| row.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-${tag.color}-100 text-${tag.color}-800"> | |
| ${tag.name} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="w-4 h-4 rounded-full bg-${tag.color}-500"></div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> | |
| ${tag.count} videos | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> | |
| <button class="text-indigo-600 hover:text-indigo-900 mr-3" onclick="editTag(${tag.id})"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| <button class="text-red-600 hover:text-red-900" onclick="confirmDeleteTag(${tag.id})"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| </td> | |
| `; | |
| tagsTable.appendChild(row); | |
| }); | |
| } | |
| // Filter tags based on search input | |
| function filterTags() { | |
| const searchTerm = tagSearch.value.toLowerCase(); | |
| document.querySelectorAll('#tagsTable tr').forEach(row => { | |
| if (row.cells.length === 1) return; // Skip the "no tags" row | |
| const tagName = row.cells[0].textContent.toLowerCase(); | |
| if (tagName.includes(searchTerm)) { | |
| row.style.display = ''; | |
| } else { | |
| row.style.display = 'none'; | |
| } | |
| }); | |
| } | |
| // Show add tag modal | |
| function showAddTagModal() { | |
| newTagName.value = ''; | |
| selectedTagColor.value = 'indigo'; | |
| // Reset color selection | |
| document.querySelectorAll('[data-color]').forEach(item => { | |
| item.classList.remove('border-gray-300'); | |
| item.classList.add('border-transparent'); | |
| }); | |
| // Select indigo by default | |
| document.querySelector('[data-color="indigo"]').classList.add('border-gray-300'); | |
| addTagModal.classList.remove('hidden'); | |
| } | |
| // Add new tag handler | |
| function addNewTagHandler() { | |
| const name = newTagName.value.trim(); | |
| const color = selectedTagColor.value; | |
| if (!name) { | |
| showToast('Please enter a tag name'); | |
| return; | |
| } | |
| if (tags.some(tag => tag.name === name)) { | |
| showToast('Tag already exists'); | |
| return; | |
| } | |
| const newTag = { | |
| id: tags.length > 0 ? Math.max(...tags.map(t => t.id)) + 1 : 1, | |
| name, | |
| color, | |
| count: 0 | |
| }; | |
| tags.push(newTag); | |
| renderTagsTable(); | |
| renderPopularTags(); | |
| addTagModal.classList.add('hidden'); | |
| showToast('Tag added successfully'); | |
| } | |
| // Edit tag | |
| function editTag(tagId) { | |
| // In a real app, this would open an edit modal | |
| showToast('Edit tag feature coming soon!'); | |
| } | |
| // Confirm tag deletion | |
| function confirmDeleteTag(tagId) { | |
| const tag = tags.find(t => t.id === tagId); | |
| if (!tag) return; | |
| if (tag.count > 0) { | |
| showToast('Cannot delete tag that is in use by videos'); | |
| return; | |
| } | |
| confirmModalTitle.textContent = 'Delete Tag'; | |
| confirmModalMessage.textContent = `Are you sure you want to delete the tag "${tag.name}"? This action cannot be undone.`; | |
| actionToConfirm = () => { | |
| tags = tags.filter(t => t.id !== tagId); | |
| renderTagsTable(); | |
| renderPopularTags(); | |
| confirmModal.classList.add('hidden'); | |
| showToast('Tag deleted successfully'); | |
| }; | |
| confirmModal.classList.remove('hidden'); | |
| } | |
| // Perform confirmed action | |
| function performConfirmedAction() { | |
| if (actionToConfirm) { | |
| actionToConfirm(); | |
| actionToConfirm = null; | |
| } | |
| confirmModal.classList.add('hidden'); | |
| } | |
| // Upload section functions | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| function highlight() { | |
| dropArea.classList.add('dragover'); | |
| } | |
| function unhighlight() { | |
| dropArea.classList.remove('dragover'); | |
| } | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| handleFiles({ target: { files } }); | |
| } | |
| function handleFiles(e) { | |
| filesToUpload = [...e.target.files]; | |
| renderUploadQueue(); | |
| } | |
| function renderUploadQueue() { | |
| uploadQueue.innerHTML = ''; | |
| if (filesToUpload.length === 0) { | |
| uploadQueue.innerHTML = ` | |
| <tr> | |
| <td colspan="4" class="px-6 py-4 text-center text-gray- | |
| </html> |