Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Видеоплатформа</title>
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
min-height: 100vh;
}
.header {
position: fixed;
top: 0; left: 0; right: 0;
background: #0f0f0f;
border-bottom: 1px solid #303030;
padding: 12px 20px;
z-index: 100;
display: flex;
align-items: center;
gap: 20px;
}
.logo {
font-size: 20px;
font-weight: bold;
color: #ff0000;
text-decoration: none;
}
.search-box {
flex: 1;
max-width: 600px;
}
.search-box input {
width: 100%;
padding: 8px 16px;
border: 1px solid #303030;
border-radius: 20px;
background: #121212;
color: #fff;
font-size: 14px;
outline: none;
}
.search-box input:focus {
border-color: #3ea6ff;
}
.container {
padding: 80px 20px 40px;
max-width: 1600px;
margin: 0 auto;
}
.video-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.video-card {
cursor: pointer;
transition: transform 0.2s;
border-radius: 12px;
overflow: hidden;
}
.video-card:hover {
transform: scale(1.02);
}
.thumbnail-wrapper {
position: relative;
aspect-ratio: 16/9;
background: #1a1a1a;
border-radius: 12px;
overflow: hidden;
}
.thumbnail-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
}
.duration {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0,0,0,0.8);
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.video-info {
padding: 12px 4px;
}
.video-title {
font-size: 14px;
font-weight: 500;
line-height: 1.4;
margin-bottom: 6px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.video-meta {
font-size: 12px;
color: #aaa;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.video-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.loading {
text-align: center;
padding: 60px;
color: #aaa;
font-size: 16px;
}
.error {
text-align: center;
padding: 60px;
color: #ff6b6b;
font-size: 16px;
}
.no-videos {
text-align: center;
padding: 60px;
color: #aaa;
font-size: 16px;
}
/* Модальное окно просмотра */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.95);
z-index: 1000;
overflow-y: auto;
}
.modal-overlay.active {
display: block;
}
.modal-content {
max-width: 1200px;
margin: 20px auto;
padding: 0 20px;
}
.video-player-wrapper {
position: relative;
aspect-ratio: 16/9;
background: #000;
border-radius: 12px;
overflow: hidden;
}
.video-player-wrapper video {
width: 100%;
height: 100%;
}
.close-btn {
position: fixed;
top: 20px; right: 20px;
background: rgba(255,255,255,0.1);
border: none;
color: #fff;
width: 40px; height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
background: rgba(255,255,255,0.2);
}
.video-details {
padding: 20px 0;
}
.video-details h1 {
font-size: 20px;
margin-bottom: 12px;
}
.video-details .meta {
color: #aaa;
font-size: 14px;
margin-bottom: 16px;
}
.video-details .description {
background: #1a1a1a;
padding: 16px;
border-radius: 12px;
font-size: 14px;
line-height: 1.6;
color: #e0e0e0;
}
/* Мобильная адаптация */
@media (max-width: 768px) {
.video-grid {
grid-template-columns: 1fr;
}
.header {
padding: 10px 12px;
}
.search-box {
display: none;
}
.container {
padding: 70px 12px 20px;
}
.video-title {
font-size: 15px;
}
.modal-content {
margin: 0;
padding: 0;
}
.video-player-wrapper {
border-radius: 0;
}
.close-btn {
top: 10px; right: 10px;
}
}
@media (min-width: 769px) and (max-width: 1200px) {
.video-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
</head>
<body>
<header class="header">
<a href="#" class="logo">▶ VideoHub</a>
<div class="search-box">
<input type="text" id="searchInput" placeholder="Поиск видео..." oninput="searchVideos()">
</div>
</header>
<div class="container">
<div id="videoGrid" class="video-grid">
<div class="loading">Загрузка видео...</div>
</div>
</div>
<!-- Модальное окно просмотра -->
<div id="videoModal" class="modal-overlay" onclick="closeModal(event)">
<button class="close-btn" onclick="closeModal()">×</button>
<div class="modal-content" onclick="event.stopPropagation()">
<div class="video-player-wrapper">
<video id="modalVideo" controls autoplay></video>
</div>
<div class="video-details">
<h1 id="modalTitle"></h1>
<div class="meta" id="modalMeta"></div>
<div class="description" id="modalDescription"></div>
</div>
</div>
</div>
<script>
const SUPABASE_URL = 'https://nwvkbmeklotwktyrcwyb.supabase.co';
const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im53dmtibWVrbG90d2t0eXJjd3liIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODE5ODc4MTksImV4cCI6MjA5NzU2MzgxOX0.JzpUeln4QXJkJVO6guq1EIrSLs6lzNlTxWMXzhnxhHs';
const supabase = supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
let allVideos = [];
async function loadVideos() {
const grid = document.getElementById('videoGrid');
try {
const { data, error } = await supabase
.from('videos')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
allVideos = data || [];
renderVideos(allVideos);
} catch (err) {
grid.innerHTML = `<div class="error">Ошибка загрузки: ${escapeHtml(err.message)}</div>`;
}
}
function renderVideos(videos) {
const grid = document.getElementById('videoGrid');
if (videos.length === 0) {
grid.innerHTML = '<div class="no-videos">Пока нет видео</div>';
return;
}
grid.innerHTML = videos.map(video => {
const date = new Date(video.created_at);
const dateStr = formatDate(date);
const thumbUrl = video.thumbnail_url || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180" fill="%231a1a1a"><text x="50%" y="50%" text-anchor="middle" fill="%23666" font-size="14">Нет превью</text></svg>';
return `
<div class="video-card" onclick="openVideo('${video.id}')">
<div class="thumbnail-wrapper">
<img src="${escapeHtml(thumbUrl)}" alt="${escapeHtml(video.title || 'Без названия')}" loading="lazy">
<span class="duration">${escapeHtml(video.duration || '0:00')}</span>
</div>
<div class="video-info">
<div class="video-title">${escapeHtml(video.title || 'Без названия')}</div>
<div class="video-meta">
<span>${escapeHtml(video.author || 'Admin')}</span>
<span>•</span>
<span>${dateStr}</span>
</div>
</div>
</div>
`;
}).join('');
}
function openVideo(id) {
const video = allVideos.find(v => v.id === id);
if (!video) return;
const modal = document.getElementById('videoModal');
const videoEl = document.getElementById('modalVideo');
const date = new Date(video.created_at);
document.getElementById('modalTitle').textContent = video.title || 'Без названия';
document.getElementById('modalMeta').innerHTML = `
${escapeHtml(video.author || 'Admin')} •
${formatDate(date)} •
${escapeHtml(video.duration || '0:00')}
`;
document.getElementById('modalDescription').textContent = video.description || 'Нет описания';
videoEl.src = video.video_url;
videoEl.poster = video.thumbnail_url || '';
modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeModal(event) {
if (event && event.target !== event.currentTarget && event.target.className !== 'close-btn') return;
const modal = document.getElementById('videoModal');
const videoEl = document.getElementById('modalVideo');
videoEl.pause();
videoEl.src = '';
modal.classList.remove('active');
document.body.style.overflow = '';
}
function searchVideos() {
const query = document.getElementById('searchInput').value.toLowerCase().trim();
if (!query) {
renderVideos(allVideos);
return;
}
const filtered = allVideos.filter(v =>
(v.title || '').toLowerCase().includes(query) ||
(v.description || '').toLowerCase().includes(query) ||
(v.author || '').toLowerCase().includes(query)
);
renderVideos(filtered);
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(date) {
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return 'только что';
if (minutes < 60) return `${minutes} мин. назад`;
if (hours < 24) return `${hours} ч. назад`;
if (days < 7) return `${days} дн. назад`;
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' });
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
});
loadVideos();
</script>
</body>
</html>