workout-archiver / index.html
frucht's picture
Add 3 files
0dd54d2 verified
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workout Video Archiver</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>
.video-upload-area {
border: 2px dashed #ccc;
transition: all 0.3s;
}
.video-upload-area:hover {
border-color: #4f46e5;
background-color: rgba(79, 70, 229, 0.05);
}
.video-upload-area.dragover {
border-color: #4f46e5;
background-color: rgba(79, 70, 229, 0.1);
}
.video-thumbnail {
aspect-ratio: 16/9;
background-color: #f3f4f6;
transition: transform 0.2s;
}
.video-thumbnail:hover {
transform: scale(1.02);
}
.tag {
transition: all 0.2s;
}
.tag:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.dark .video-thumbnail {
background-color: #1f2937;
}
.dark .tag {
background-color: #374151;
}
.dark .video-upload-area {
border-color: #4b5563;
}
.dark .video-upload-area:hover {
border-color: #818cf8;
background-color: rgba(129, 140, 248, 0.05);
}
.dark .video-upload-area.dragover {
border-color: #818cf8;
background-color: rgba(129, 140, 248, 0.1);
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dark ::-webkit-scrollbar-track {
background: #1f2937;
}
.dark ::-webkit-scrollbar-thumb {
background: #4b5563;
}
.dark ::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Loading spinner */
.spinner {
width: 24px;
height: 24px;
border: 3px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 transition-colors duration-200 min-h-screen">
<div class="container mx-auto px-4 py-6">
<!-- Header -->
<header class="flex justify-between items-center mb-8">
<div>
<h1 class="text-3xl font-bold text-indigo-600 dark:text-indigo-400">Workout Video Archiver</h1>
<p class="text-gray-600 dark:text-gray-400">Organize and share your training videos</p>
</div>
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600">
<i class="fas fa-moon dark:hidden"></i>
<i class="fas fa-sun hidden dark:inline"></i>
</button>
<div id="profile-section">
<div class="relative">
<img id="profile-pic" src="https://randomuser.me/api/portraits/men/32.jpg" alt="Profile" class="w-10 h-10 rounded-full cursor-pointer">
<div class="absolute right-0 bottom-0 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-gray-900"></div>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<!-- Sidebar -->
<aside class="lg:col-span-1 space-y-6">
<!-- Navigation -->
<nav class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<ul class="space-y-2">
<li>
<a href="#" class="flex items-center space-x-3 p-2 rounded-lg bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300">
<i class="fas fa-home w-5"></i>
<span>Dashboard</span>
</a>
</li>
<li>
<a href="#" class="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-video w-5"></i>
<span>My Videos</span>
</a>
</li>
<li>
<a href="#" class="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-tags w-5"></i>
<span>Tags</span>
</a>
</li>
<li>
<a href="#" class="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-chart-line w-5"></i>
<span>Analytics</span>
</a>
</li>
<li>
<a href="#" class="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<i class="fas fa-cog w-5"></i>
<span>Settings</span>
</a>
</li>
</ul>
</nav>
<!-- Quick Stats -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h3 class="font-medium mb-3">Quick Stats</h3>
<div class="space-y-3">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Total Videos</p>
<p id="total-videos" class="text-xl font-semibold">0</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Workouts</p>
<p id="total-workouts" class="text-xl font-semibold">0</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Tags</p>
<p id="total-tags" class="text-xl font-semibold">0</p>
</div>
</div>
</div>
<!-- Recent Tags -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h3 class="font-medium mb-3">Recent Tags</h3>
<div id="recent-tags" class="flex flex-wrap gap-2">
<!-- Tags will be loaded here -->
</div>
</div>
</aside>
<!-- Main Area -->
<main class="lg:col-span-3 space-y-6">
<!-- Upload Section -->
<section class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold">Upload New Video</h2>
<div class="flex space-x-2">
<button id="record-btn" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg flex items-center space-x-2">
<i class="fas fa-video"></i>
<span>Record</span>
</button>
<button id="upload-btn" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded-lg flex items-center space-x-2">
<i class="fas fa-upload"></i>
<span>Upload</span>
</button>
</div>
</div>
<div id="upload-area" class="video-upload-area rounded-lg p-8 text-center cursor-pointer">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-3"></i>
<p class="font-medium">Drag & drop your video here</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">or click to browse files</p>
<input type="file" id="file-input" accept="video/*" class="hidden">
</div>
</section>
<!-- Filters -->
<section class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-4">
<h2 class="text-xl font-semibold">My Videos</h2>
<div class="flex flex-col sm:flex-row gap-3">
<div class="relative">
<select id="sort-select" class="appearance-none bg-gray-100 dark:bg-gray-700 border-0 rounded-lg pl-4 pr-8 py-2 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
<option value="date">Sort by Date</option>
<option value="name">Sort by Name</option>
<option value="workout">Sort by Workout</option>
</select>
<i class="fas fa-chevron-down absolute right-3 top-3 text-gray-500 pointer-events-none"></i>
</div>
<div class="relative">
<select id="workout-filter" class="appearance-none bg-gray-100 dark:bg-gray-700 border-0 rounded-lg pl-4 pr-8 py-2 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
<option value="all">All Workouts</option>
<option value="strength">Strength Training</option>
<option value="hiit">HIIT</option>
<option value="yoga">Yoga</option>
<option value="cardio">Cardio</option>
</select>
<i class="fas fa-chevron-down absolute right-3 top-3 text-gray-500 pointer-events-none"></i>
</div>
<div class="relative">
<input id="search-input" type="text" placeholder="Search videos..." class="bg-gray-100 dark:bg-gray-700 border-0 rounded-lg pl-4 pr-10 py-2 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
<i class="fas fa-search absolute right-3 top-3 text-gray-500 pointer-events-none"></i>
</div>
</div>
</div>
<div id="active-filters" class="flex flex-wrap gap-3 mb-4">
<!-- Active filters will appear here -->
</div>
</section>
<!-- Video Grid -->
<section id="video-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Videos will be loaded here -->
<div id="loading-spinner" class="col-span-full flex justify-center py-8 hidden">
<div class="spinner"></div>
</div>
<div id="no-videos" class="col-span-full text-center py-8 text-gray-500 dark:text-gray-400">
<i class="fas fa-video-slash text-4xl mb-3"></i>
<p class="text-lg">No videos found</p>
<p class="text-sm">Upload your first workout video to get started</p>
</div>
</section>
</main>
</div>
</div>
<!-- Video Modal -->
<div id="video-modal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-gray-800 rounded-lg w-full max-w-4xl mx-4 relative">
<button id="close-modal" class="absolute -top-10 right-0 text-white hover:text-gray-300">
<i class="fas fa-times text-2xl"></i>
</button>
<div class="p-4">
<video id="modal-video" controls class="w-full rounded-lg">
Your browser does not support the video tag.
</video>
</div>
<div class="p-6">
<h3 id="modal-title" class="text-xl font-semibold mb-2"></h3>
<p id="modal-description" class="text-gray-600 dark:text-gray-400 mb-4"></p>
<div class="flex flex-wrap gap-2 mb-4" id="modal-tags"></div>
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500 dark:text-gray-400">
<span id="modal-date"></span><span id="modal-duration"></span>
</div>
<div class="flex space-x-3">
<button id="whatsapp-share" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg flex items-center space-x-2">
<i class="fab fa-whatsapp"></i>
<span>WhatsApp</span>
</button>
<button id="email-share" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded-lg flex items-center space-x-2">
<i class="fas fa-envelope"></i>
<span>Email</span>
</button>
<button id="copy-link" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded-lg flex items-center space-x-2">
<i class="fas fa-link"></i>
<span>Copy Link</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Upload Modal -->
<div id="upload-modal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-gray-800 rounded-lg w-full max-w-2xl mx-4 relative">
<button id="close-upload-modal" class="absolute -top-10 right-0 text-white hover:text-gray-300">
<i class="fas fa-times text-2xl"></i>
</button>
<div class="p-6">
<h3 class="text-xl font-semibold mb-4">Upload New Workout Video</h3>
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Video File</label>
<div id="modal-upload-area" class="video-upload-area rounded-lg p-8 text-center cursor-pointer mb-2">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-3"></i>
<p class="font-medium">Drag & drop your video here</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">or click to browse files</p>
<input type="file" id="modal-file-input" accept="video/*" class="hidden">
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">MP4, MOV or AVI. Max 500MB.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label for="video-title" class="block text-sm font-medium mb-2">Video Title</label>
<input type="text" id="video-title" class="w-full bg-gray-100 dark:bg-gray-700 border-0 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
</div>
<div>
<label for="workout-name" class="block text-sm font-medium mb-2">Workout Name</label>
<input type="text" id="workout-name" class="w-full bg-gray-100 dark:bg-gray-700 border-0 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
</div>
<div>
<label for="training-name" class="block text-sm font-medium mb-2">Training Program</label>
<input type="text" id="training-name" class="w-full bg-gray-100 dark:bg-gray-700 border-0 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
</div>
<div>
<label for="video-date" class="block text-sm font-medium mb-2">Date</label>
<input type="date" id="video-date" class="w-full bg-gray-100 dark:bg-gray-700 border-0 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
</div>
</div>
<div class="mb-6">
<label for="video-description" class="block text-sm font-medium mb-2">Description</label>
<textarea id="video-description" rows="3" class="w-full bg-gray-100 dark:bg-gray-700 border-0 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:outline-none"></textarea>
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Tags</label>
<div class="flex flex-wrap gap-2 mb-2" id="selected-tags">
<!-- Tags will be added here -->
</div>
<div class="flex">
<input type="text" id="tag-input" placeholder="Add a tag" class="flex-1 bg-gray-100 dark:bg-gray-700 border-0 rounded-l-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
<button id="add-tag-btn" class="px-4 bg-indigo-600 text-white rounded-r-lg hover:bg-indigo-700">Add</button>
</div>
<div class="flex flex-wrap gap-2 mt-2">
<span class="tag px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-xs cursor-pointer">strength</span>
<span class="tag px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-xs cursor-pointer">hiit</span>
<span class="tag px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-xs cursor-pointer">beginner</span>
<span class="tag px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-xs cursor-pointer">advanced</span>
<span class="tag px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-xs cursor-pointer">core</span>
<span class="tag px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-xs cursor-pointer">upper body</span>
</div>
</div>
<div class="flex justify-end space-x-3">
<button id="cancel-upload" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded-lg">
Cancel
</button>
<button id="save-video" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg flex items-center space-x-2">
<i class="fas fa-save"></i>
<span>Save Video</span>
</button>
</div>
</div>
</div>
</div>
<!-- Loading Modal -->
<div id="loading-modal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 text-center">
<div class="spinner mx-auto mb-4"></div>
<p id="loading-text" class="text-lg font-medium">Uploading video...</p>
</div>
</div>
<script>
// Global variables
let videos = [];
let selectedFile = null;
// DOM elements
const videoGrid = document.getElementById('video-grid');
const loadingSpinner = document.getElementById('loading-spinner');
const noVideos = document.getElementById('no-videos');
const totalVideos = document.getElementById('total-videos');
const totalWorkouts = document.getElementById('total-workouts');
const totalTags = document.getElementById('total-tags');
const recentTags = document.getElementById('recent-tags');
const activeFilters = document.getElementById('active-filters');
const sortSelect = document.getElementById('sort-select');
const workoutFilter = document.getElementById('workout-filter');
const searchInput = document.getElementById('search-input');
// Load videos from localStorage
function loadVideos() {
try {
loadingSpinner.classList.remove('hidden');
noVideos.classList.add('hidden');
// Load from localStorage
const savedVideos = localStorage.getItem('workoutVideos');
if (savedVideos) {
videos = JSON.parse(savedVideos);
} else {
// Default demo videos
videos = [
{
id: '1',
title: "Full Body Strength Workout - Week 1",
description: "Complete full body strength workout focusing on compound movements. Perfect for beginners starting their strength journey.",
tags: ["strength", "beginner", "full body"],
date: "2 days ago",
duration: "4:32",
thumbnail: "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=800&q=80",
videoUrl: "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4",
workoutType: "strength",
uploadDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: '2',
title: "Advanced HIIT Circuit - Fat Burning",
description: "High intensity interval training circuit designed to maximize fat burning and improve cardiovascular endurance.",
tags: ["hiit", "advanced", "cardio", "fat loss"],
date: "1 week ago",
duration: "12:15",
thumbnail: "https://images.unsplash.com/photo-1571019614242-c5c5a73d6587?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=800&q=80",
videoUrl: "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4",
workoutType: "hiit",
uploadDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
},
{
id: '3',
title: "Morning Yoga Flow - Flexibility Routine",
description: "Gentle yoga flow perfect for mornings to improve flexibility, mobility and start your day with energy.",
tags: ["yoga", "flexibility", "morning"],
date: "2 weeks ago",
duration: "8:45",
thumbnail: "https://images.unsplash.com/photo-1545205597-3d9d02c29597?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=800&q=80",
videoUrl: "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4",
workoutType: "yoga",
uploadDate: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString()
}
];
// Save to localStorage
localStorage.setItem('workoutVideos', JSON.stringify(videos));
}
updateStats();
renderVideos();
loadingSpinner.classList.add('hidden');
if (videos.length === 0) {
noVideos.classList.remove('hidden');
noVideos.innerHTML = `
<i class="fas fa-video-slash text-4xl mb-3"></i>
<p class="text-lg">No videos found</p>
<p class="text-sm">Upload your first workout video to get started</p>
`;
}
} catch (error) {
console.error('Error loading videos:', error);
loadingSpinner.classList.add('hidden');
alert('Failed to load videos. Please try again.');
}
}
// Save videos to localStorage
function saveVideosToStorage() {
localStorage.setItem('workoutVideos', JSON.stringify(videos));
}
// Update statistics
function updateStats() {
totalVideos.textContent = videos.length;
// Count unique workout types
const workoutTypes = new Set(videos.map(video => video.workoutType));
totalWorkouts.textContent = workoutTypes.size;
// Count unique tags
const allTags = videos.flatMap(video => video.tags);
const uniqueTags = new Set(allTags);
totalTags.textContent = uniqueTags.size;
// Update recent tags
recentTags.innerHTML = '';
const tagCounts = {};
allTags.forEach(tag => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
});
const sortedTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);
sortedTags.slice(0, 6).forEach(tag => {
const tagElement = document.createElement('span');
tagElement.className = 'tag px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-sm';
tagElement.textContent = tag;
recentTags.appendChild(tagElement);
});
}
// Render videos to the grid
function renderVideos() {
videoGrid.innerHTML = '';
// Filter and sort videos
let filteredVideos = [...videos];
// Apply search filter
const searchTerm = searchInput.value.toLowerCase();
if (searchTerm) {
filteredVideos = filteredVideos.filter(video =>
video.title.toLowerCase().includes(searchTerm) ||
video.description.toLowerCase().includes(searchTerm) ||
video.tags.some(tag => tag.toLowerCase().includes(searchTerm))
);
}
// Apply workout type filter
const workoutType = workoutFilter.value;
if (workoutType !== 'all') {
filteredVideos = filteredVideos.filter(video => video.workoutType === workoutType);
}
// Apply sorting
const sortBy = sortSelect.value;
if (sortBy === 'date') {
filteredVideos.sort((a, b) => new Date(b.uploadDate) - new Date(a.uploadDate));
} else if (sortBy === 'name') {
filteredVideos.sort((a, b) => a.title.localeCompare(b.title));
} else if (sortBy === 'workout') {
filteredVideos.sort((a, b) => a.workoutType.localeCompare(b.workoutType));
}
// Update active filters
updateActiveFilters();
// Render videos
if (filteredVideos.length === 0) {
noVideos.classList.remove('hidden');
} else {
noVideos.classList.add('hidden');
filteredVideos.forEach((video, index) => {
const videoCard = createVideoCard(video, index);
videoGrid.appendChild(videoCard);
});
}
// Add "Add New Video" card
const addCard = document.createElement('div');
addCard.className = 'bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden hover:shadow-lg transition-shadow flex items-center justify-center';
addCard.innerHTML = `
<button class="w-full h-full p-8 flex flex-col items-center justify-center text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300">
<i class="fas fa-plus-circle text-4xl mb-3"></i>
<span class="font-medium">Add New Video</span>
</button>
`;
addCard.querySelector('button').addEventListener('click', () => {
document.getElementById('upload-modal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
});
videoGrid.appendChild(addCard);
}
// Create a video card element
function createVideoCard(video, index) {
const card = document.createElement('div');
card.className = 'bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden hover:shadow-lg transition-shadow';
card.innerHTML = `
<div class="video-thumbnail relative">
<img src="${video.thumbnail}" alt="Workout Video" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<button class="w-12 h-12 bg-white bg-opacity-80 rounded-full flex items-center justify-center text-indigo-600 hover:bg-opacity-100">
<i class="fas fa-play"></i>
</button>
</div>
<div class="absolute bottom-2 right-2 bg-black bg-opacity-70 text-white text-xs px-2 py-1 rounded">
${video.duration}
</div>
</div>
<div class="p-4">
<div class="flex justify-between items-start mb-2">
<h3 class="font-semibold line-clamp-1">${video.title}</h3>
<div class="dropdown relative">
<button class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg z-10 hidden">
<div class="py-1">
<a href="#" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Edit</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Share</a>
<a href="#" class="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-700">Delete</a>
</div>
</div>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">${video.description}</p>
<div class="flex flex-wrap gap-2 mb-3">
${video.tags.map(tag => `<span class="tag px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-xs">${tag}</span>`).join('')}
</div>
<div class="flex justify-between items-center text-xs text-gray-500 dark:text-gray-400">
<span>${video.date}</span>
<div class="flex space-x-2">
<button class="hover:text-indigo-600 dark:hover:text-indigo-400">
<i class="fas fa-share-alt"></i>
</button>
<button class="hover:text-indigo-600 dark:hover:text-indigo-400">
<i class="fas fa-download"></i>
</button>
</div>
</div>
</div>
`;
// Add click handler for play button
card.querySelector('.video-thumbnail button').addEventListener('click', () => {
openVideoModal(video);
});
// Add dropdown menu functionality
const dropdownBtn = card.querySelector('.dropdown button');
const dropdownMenu = card.querySelector('.dropdown-menu');
dropdownBtn.addEventListener('click', (e) => {
e.stopPropagation();
dropdownMenu.classList.toggle('hidden');
});
// Add click handler for edit button
const editBtn = card.querySelector('.dropdown-menu a:first-child');
editBtn.addEventListener('click', (e) => {
e.preventDefault();
openEditModal(video);
});
// Add click handler for delete button
const deleteBtn = card.querySelector('.dropdown-menu a:last-child');
deleteBtn.addEventListener('click', (e) => {
e.preventDefault();
if (confirm('Are you sure you want to delete this video?')) {
deleteVideo(video.id);
}
});
return card;
}
// Update active filters display
function updateActiveFilters() {
activeFilters.innerHTML = '';
// Add search filter if applicable
const searchTerm = searchInput.value.trim();
if (searchTerm) {
const filter = document.createElement('span');
filter.className = 'tag px-3 py-1 bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 rounded-full text-sm flex items-center';
filter.innerHTML = `
<span>Search: "${searchTerm}"</span>
<button class="ml-2 text-indigo-500 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300">
<i class="fas fa-times"></i>
</button>
`;
filter.querySelector('button').addEventListener('click', () => {
searchInput.value = '';
renderVideos();
});
activeFilters.appendChild(filter);
}
// Add workout type filter if applicable
const workoutType = workoutFilter.value;
if (workoutType !== 'all') {
const filter = document.createElement('span');
filter.className = 'tag px-3 py-1 bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 rounded-full text-sm flex items-center';
filter.innerHTML = `
<span>Workout: ${workoutType.charAt(0).toUpperCase() + workoutType.slice(1)}</span>
<button class="ml-2 text-indigo-500 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300">
<i class="fas fa-times"></i>
</button>
`;
filter.querySelector('button').addEventListener('click', () => {
workoutFilter.value = 'all';
renderVideos();
});
activeFilters.appendChild(filter);
}
// Add sort filter if applicable
const sortBy = sortSelect.value;
if (sortBy !== 'date') {
const filter = document.createElement('span');
filter.className = 'tag px-3 py-1 bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 rounded-full text-sm flex items-center';
filter.innerHTML = `
<span>Sorted by: ${sortBy.charAt(0).toUpperCase() + sortBy.slice(1)}</span>
<button class="ml-2 text-indigo-500 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300">
<i class="fas fa-times"></i>
</button>
`;
filter.querySelector('button').addEventListener('click', () => {
sortSelect.value = 'date';
renderVideos();
});
activeFilters.appendChild(filter);
}
// Add "Clear All" button if there are active filters
if (activeFilters.children.length > 0) {
const clearAll = document.createElement('button');
clearAll.className = 'text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 flex items-center';
clearAll.innerHTML = `
<i class="fas fa-times mr-1"></i>
<span>Clear All</span>
`;
clearAll.addEventListener('click', () => {
searchInput.value = '';
workoutFilter.value = 'all';
sortSelect.value = 'date';
renderVideos();
});
activeFilters.appendChild(clearAll);
}
}
// Open video modal
function openVideoModal(video) {
const videoModal = document.getElementById('video-modal');
const modalVideo = document.getElementById('modal-video');
const modalTitle = document.getElementById('modal-title');
const modalDescription = document.getElementById('modal-description');
const modalTags = document.getElementById('modal-tags');
const modalDate = document.getElementById('modal-date');
const modalDuration = document.getElementById('modal-duration');
modalVideo.src = video.videoUrl;
modalTitle.textContent = video.title;
modalDescription.textContent = video.description;
modalDate.textContent = video.date;
modalDuration.textContent = video.duration;
// Clear previous tags
modalTags.innerHTML = '';
// Add new tags
video.tags.forEach(tag => {
const tagElement = document.createElement('span');
tagElement.className = 'tag px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-xs';
tagElement.textContent = tag;
modalTags.appendChild(tagElement);
});
// Add share button handlers
document.getElementById('whatsapp-share').addEventListener('click', () => {
shareVideo(video, 'whatsapp');
});
document.getElementById('email-share').addEventListener('click', () => {
shareVideo(video, 'email');
});
document.getElementById('copy-link').addEventListener('click', () => {
shareVideo(video, 'link');
});
videoModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
// Share video
function shareVideo(video, method) {
let message = `Check out my workout video: ${video.title}\n\n${video.description}\n\nTags: ${video.tags.join(', ')}`;
switch (method) {
case 'whatsapp':
window.open(`https://wa.me/?text=${encodeURIComponent(message)}`);
break;
case 'email':
window.open(`mailto:?subject=${encodeURIComponent(video.title)}&body=${encodeURIComponent(message)}`);
break;
case 'link':
navigator.clipboard.writeText(video.videoUrl).then(() => {
alert('Video link copied to clipboard!');
});
break;
}
}
// Open edit modal
function openEditModal(video) {
const uploadModal = document.getElementById('upload-modal');
const modalTitle = uploadModal.querySelector('h3');
const videoTitle = document.getElementById('video-title');
const workoutName = document.getElementById('workout-name');
const trainingName = document.getElementById('training-name');
const videoDate = document.getElementById('video-date');
const videoDescription = document.getElementById('video-description');
const selectedTags = document.getElementById('selected-tags');
const modalUploadArea = document.getElementById('modal-upload-area');
modalTitle.textContent = 'Edit Video';
videoTitle.value = video.title;
workoutName.value = video.workoutType;
trainingName.value = '';
videoDate.value = '';
videoDescription.value = video.description;
// Clear previous tags
selectedTags.innerHTML = '';
// Add current tags
video.tags.forEach(tag => {
addTagToForm(tag);
});
// Update upload area to show current video
modalUploadArea.innerHTML = `
<i class="fas fa-check-circle text-4xl text-green-500 mb-3"></i>
<p class="font-medium">Current Video: ${video.title}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Click to change file</p>
`;
// Store the video being edited
selectedFile = { name: video.title };
uploadModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
// Delete video
function deleteVideo(videoId) {
videos = videos.filter(video => video.id !== videoId);
saveVideosToStorage();
updateStats();
renderVideos();
}
// Upload video (save to localStorage)
async function uploadVideo(file, metadata) {
try {
// Show loading modal
const loadingModal = document.getElementById('loading-modal');
const loadingText = document.getElementById('loading-text');
loadingText.textContent = 'Uploading video...';
loadingModal.classList.remove('hidden');
// Simulate upload delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Create a new video object
const newVideo = {
id: Date.now().toString(),
title: metadata.title,
description: metadata.description,
tags: metadata.tags,
date: "Just now",
duration: "5:00", // In a real app, you would get this from the video
thumbnail: "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=800&q=80",
videoUrl: URL.createObjectURL(file),
workoutType: metadata.workoutType,
uploadDate: new Date().toISOString()
};
// Add to videos array
videos.unshift(newVideo);
// Save to localStorage
saveVideosToStorage();
// Update UI
updateStats();
renderVideos();
// Hide loading modal
loadingModal.classList.add('hidden');
// Show success message
alert('Video uploaded successfully!');
// Close upload modal
document.getElementById('upload-modal').classList.add('hidden');
document.body.style.overflow = 'auto';
// Reset form
resetUploadForm();
} catch (error) {
console.error('Error uploading video:', error);
loadingModal.classList.add('hidden');
alert('Failed to upload video. Please try again.');
}
}
// Reset upload form
function resetUploadForm() {
document.getElementById('video-title').value = '';
document.getElementById('workout-name').value = '';
document.getElementById('training-name').value = '';
document.getElementById('video-date').value = '';
document.getElementById('video-description').value = '';
document.getElementById('selected-tags').innerHTML = '';
document.getElementById('modal-upload-area').innerHTML = `
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-3"></i>
<p class="font-medium">Drag & drop your video here</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">or click to browse files</p>
`;
selectedFile = null;
}
// Add tag to form
function addTagToForm(tagText) {
const tagElement = document.createElement('span');
tagElement.className = 'tag px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-xs flex items-center';
tagElement.innerHTML = `
<span>${tagText}</span>
<button class="ml-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times text-xs"></i>
</button>
`;
const removeBtn = tagElement.querySelector('button');
removeBtn.addEventListener('click', () => {
tagElement.remove();
});
document.getElementById('selected-tags').appendChild(tagElement);
}
// Initialize event listeners
function initEventListeners() {
// Theme toggle
const themeToggle = document.getElementById('theme-toggle');
themeToggle.addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', document.documentElement.classList.contains('dark'));
});
// Check for saved theme preference
if (localStorage.getItem('darkMode') === 'true') {
document.documentElement.classList.add('dark');
} else if (localStorage.getItem('darkMode') === 'false') {
document.documentElement.classList.remove('dark');
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
// Video modal functionality
const videoModal = document.getElementById('video-modal');
const closeModal = document.getElementById('close-modal');
closeModal.addEventListener('click', () => {
videoModal.classList.add('hidden');
document.getElementById('modal-video').pause();
document.body.style.overflow = 'auto';
});
// Close modal when clicking outside
videoModal.addEventListener('click', (e) => {
if (e.target === videoModal) {
videoModal.classList.add('hidden');
document.getElementById('modal-video').pause();
document.body.style.overflow = 'auto';
}
});
// Upload modal functionality
const uploadModal = document.getElementById('upload-modal');
const closeUploadModal = document.getElementById('close-upload-modal');
const cancelUpload = document.getElementById('cancel-upload');
const uploadBtn = document.getElementById('upload-btn');
const recordBtn = document.getElementById('record-btn');
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('file-input');
const modalFileInput = document.getElementById('modal-file-input');
const modalUploadArea = document.getElementById('modal-upload-area');
const selectedTags = document.getElementById('selected-tags');
const tagInput = document.getElementById('tag-input');
const addTagBtn = document.getElementById('add-tag-btn');
const saveVideoBtn = document.getElementById('save-video');
// Open upload modal
uploadBtn.addEventListener('click', () => {
uploadModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
});
// Open upload modal from record button
recordBtn.addEventListener('click', () => {
// In a real app, this would open camera recording
uploadModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
});
// Close upload modal
closeUploadModal.addEventListener('click', () => {
uploadModal.classList.add('hidden');
document.body.style.overflow = 'auto';
resetUploadForm();
});
cancelUpload.addEventListener('click', () => {
uploadModal.classList.add('hidden');
document.body.style.overflow = 'auto';
resetUploadForm();
});
// Close modal when clicking outside
uploadModal.addEventListener('click', (e) => {
if (e.target === uploadModal) {
uploadModal.classList.add('hidden');
document.body.style.overflow = 'auto';
resetUploadForm();
}
});
// File upload handling
uploadArea.addEventListener('click', () => {
fileInput.click();
});
modalUploadArea.addEventListener('click', () => {
modalFileInput.click();
});
modalFileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
selectedFile = e.target.files[0];
modalUploadArea.innerHTML = `
<i class="fas fa-check-circle text-4xl text-green-500 mb-3"></i>
<p class="font-medium">${selectedFile.name}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Click to change file</p>
`;
}
});
// Drag and drop functionality
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
[uploadArea, modalUploadArea].forEach(area => {
area.addEventListener(eventName, preventDefaults, false);
});
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
[uploadArea, modalUploadArea].forEach(area => {
area.addEventListener(eventName, highlight, false);
});
});
['dragleave', 'drop'].forEach(eventName => {
[uploadArea, modalUploadArea].forEach(area => {
area.addEventListener(eventName, unhighlight, false);
});
});
function highlight() {
this.classList.add('dragover');
}
function unhighlight() {
this.classList.remove('dragover');
}
uploadArea.addEventListener('drop', handleDrop, false);
modalUploadArea.addEventListener('drop', handleModalDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
fileInput.files = files;
if (files.length > 0) {
const fileName = files[0].name;
uploadArea.innerHTML = `
<i class="fas fa-check-circle text-4xl text-green-500 mb-3"></i>
<p class="font-medium">${fileName}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Click to change file</p>
`;
}
}
function handleModalDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
modalFileInput.files = files;
if (files.length > 0) {
selectedFile = files[0];
modalUploadArea.innerHTML = `
<i class="fas fa-check-circle text-4xl text-green-500 mb-3"></i>
<p class="font-medium">${selectedFile.name}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Click to change file</p>
`;
}
}
// Tag functionality
addTagBtn.addEventListener('click', () => {
const tagText = tagInput.value.trim();
if (tagText) {
addTagToForm(tagText);
tagInput.value = '';
}
});
tagInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const tagText = tagInput.value.trim();
if (tagText) {
addTagToForm(tagText);
tagInput.value = '';
}
}
});
// Add click handler for suggested tags
document.querySelectorAll('#upload-modal .tag').forEach(tag => {
tag.addEventListener('click', () => {
const tagText = tag.textContent;
addTagToForm(tagText);
});
});
// Save video
saveVideoBtn.addEventListener('click', () => {
if (!selectedFile) {
alert('Please select a video file to upload.');
return;
}
const title = document.getElementById('video-title').value.trim();
if (!title) {
alert('Please enter a title for your video.');
return;
}
const workoutType = document.getElementById('workout-name').value.trim();
const description = document.getElementById('video-description').value.trim();
// Get selected tags
const tags = [];
document.querySelectorAll('#selected-tags span span:first-child').forEach(tag => {
tags.push(tag.textContent);
});
// Create metadata object
const metadata = {
title,
workoutType,
description,
tags
};
// Upload the video
uploadVideo(selectedFile, metadata);
});
// Dropdown menus
document.querySelectorAll('.dropdown button').forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
const menu = button.nextElementSibling;
menu.classList.toggle('hidden');
});
});
// Close dropdown when clicking outside
document.addEventListener('click', () => {
document.querySelectorAll('.dropdown-menu').forEach(menu => {
menu.classList.add('hidden');
});
});
// Filter and search functionality
sortSelect.addEventListener('change', renderVideos);
workoutFilter.addEventListener('change', renderVideos);
searchInput.addEventListener('input', renderVideos);
}
// Initialize the app
function init() {
initEventListeners();
loadVideos();
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', init);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=frucht/workout-archiver" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>