Created
July 7, 2025 20:07
-
-
Save Zamay/8e273c4b2e00454567194f8e0f1ebbb5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect, useRef } from 'react'; | |
import { Search, Film, Tv, Star, Bookmark, Home, Play, ArrowLeft, Menu, X, Loader, Download, Settings, Info, Clock, Heart, Filter, ChevronDown, ChevronUp, User, Grid, List } from 'lucide-react'; | |
const HDRezkaMobile = () => { | |
const [currentPage, setCurrentPage] = useState('home'); | |
const [searchQuery, setSearchQuery] = useState(''); | |
const [selectedCategory, setSelectedCategory] = useState('all'); | |
const [bookmarks, setBookmarks] = useState([]); | |
const [watchHistory, setWatchHistory] = useState([]); | |
const [isMenuOpen, setIsMenuOpen] = useState(false); | |
const [selectedMovie, setSelectedMovie] = useState(null); | |
const [movies, setMovies] = useState([]); | |
const [isLoading, setIsLoading] = useState(false); | |
const [currentVideoUrl, setCurrentVideoUrl] = useState(null); | |
const [showPlayer, setShowPlayer] = useState(false); | |
const [viewMode, setViewMode] = useState('grid'); | |
const [sortBy, setSortBy] = useState('rating'); | |
const [filterOpen, setFilterOpen] = useState(false); | |
const [selectedYear, setSelectedYear] = useState('all'); | |
const [selectedGenre, setSelectedGenre] = useState('all'); | |
const [isOnline, setIsOnline] = useState(navigator.onLine); | |
const [installPrompt, setInstallPrompt] = useState(null); | |
const [showInstallBanner, setShowInstallBanner] = useState(false); | |
const searchInputRef = useRef(null); | |
// Слухаємо статус мережі | |
useEffect(() => { | |
const handleOnline = () => setIsOnline(true); | |
const handleOffline = () => setIsOnline(false); | |
window.addEventListener('online', handleOnline); | |
window.addEventListener('offline', handleOffline); | |
return () => { | |
window.removeEventListener('online', handleOnline); | |
window.removeEventListener('offline', handleOffline); | |
}; | |
}, []); | |
// PWA install handler | |
useEffect(() => { | |
const handleBeforeInstallPrompt = (e) => { | |
e.preventDefault(); | |
setInstallPrompt(e); | |
setShowInstallBanner(true); | |
}; | |
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); | |
return () => { | |
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); | |
}; | |
}, []); | |
const installApp = async () => { | |
if (installPrompt) { | |
const result = await installPrompt.prompt(); | |
if (result.outcome === 'accepted') { | |
setShowInstallBanner(false); | |
} | |
setInstallPrompt(null); | |
} | |
}; | |
// Функція для парсингу контенту з HDRezka | |
const parseHDRezkaContent = async (query = '', category = 'all') => { | |
setIsLoading(true); | |
try { | |
// Симуляція запиту до HDRezka (реальний парсинг потребує проксі-сервера) | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
const demoMovies = getDemoMovies(); | |
let filteredMovies = demoMovies; | |
if (query) { | |
filteredMovies = demoMovies.filter(movie => | |
movie.title.toLowerCase().includes(query.toLowerCase()) || | |
movie.description.toLowerCase().includes(query.toLowerCase()) | |
); | |
} | |
if (category !== 'all') { | |
filteredMovies = filteredMovies.filter(movie => movie.type === category); | |
} | |
// Сортування | |
filteredMovies.sort((a, b) => { | |
switch (sortBy) { | |
case 'rating': | |
return b.rating - a.rating; | |
case 'year': | |
return b.year - a.year; | |
case 'title': | |
return a.title.localeCompare(b.title); | |
default: | |
return 0; | |
} | |
}); | |
return filteredMovies; | |
} catch (error) { | |
console.error('Помилка парсингу:', error); | |
return getDemoMovies(); | |
} finally { | |
setIsLoading(false); | |
} | |
}; | |
// Розширені демо дані | |
const getDemoMovies = () => { | |
return [ | |
{ | |
id: 1, | |
title: "Истребитель демонов", | |
type: "anime", | |
year: 2019, | |
rating: 8.7, | |
genre: "Экшен", | |
poster: "https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=300&h=450&fit=crop", | |
description: "Танджиро Камадо — добрый мальчик, который живёт со своей семьей в горах и зарабатывает на жизнь продажей угля. Однажды он возвращается домой и обнаруживает, что вся его семья была убита демоном.", | |
episodes: 26, | |
seasons: 3, | |
duration: "24м", | |
link: "https://hdrezka.me/animation/adventures/30522-istrebitel-demonov.html", | |
trailer: "https://www.youtube.com/embed/VQGCKyvzIM4", | |
cast: ["Нацуки Ханаэ", "Сатоми Сато", "Хиро Симоно"], | |
director: "Харуо Сотодзаки", | |
studio: "Ufotable" | |
}, | |
{ | |
id: 2, | |
title: "Мстители: Финал", | |
type: "movie", | |
year: 2019, | |
rating: 8.4, | |
genre: "Фантастика", | |
poster: "https://images.unsplash.com/photo-1440404653325-ab127d49abc1?w=300&h=450&fit=crop", | |
description: "Оставшиеся в живых Мстители и их союзники должны разработать новый план, который поможет им противостоять разрушительным действиям могущественного титана Таноса.", | |
duration: "3ч 1м", | |
link: "https://hdrezka.me/films/fiction/1234-avengers-endgame.html", | |
trailer: "https://www.youtube.com/embed/TcMBFSGVi1c", | |
cast: ["Роберт Дауни мл.", "Крис Эванс", "Марк Руффало"], | |
director: "Энтони Руссо, Джо Руссо", | |
studio: "Marvel Studios" | |
}, | |
{ | |
id: 3, | |
title: "Игра престолов", | |
type: "series", | |
year: 2011, | |
rating: 9.2, | |
genre: "Драма", | |
poster: "https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=300&h=450&fit=crop", | |
description: "Борьба за власть в Семи Королевствах Вестероса. Благородные семьи сражаются за контроль над Железным троном, в то время как древнее зло пробуждается на севере.", | |
seasons: 8, | |
episodes: 73, | |
duration: "57м", | |
link: "https://hdrezka.me/series/drama/5678-game-of-thrones.html", | |
trailer: "https://www.youtube.com/embed/rlR4PJn8b8I", | |
cast: ["Питер Динклэйдж", "Лена Хиди", "Эмилия Кларк"], | |
director: "Дэвид Бениофф, Д. Б. Вайсс", | |
studio: "HBO" | |
}, | |
{ | |
id: 4, | |
title: "Во все тяжкие", | |
type: "series", | |
year: 2008, | |
rating: 9.5, | |
genre: "Драма", | |
poster: "https://images.unsplash.com/photo-1489599162163-36f1e05c0c90?w=300&h=450&fit=crop", | |
description: "Школьный учитель химии Уолтер Уайт узнаёт, что болен раком лёгких. Решив обеспечить семью деньгами, он начинает варить наркотики вместе со своим бывшим учеником.", | |
seasons: 5, | |
episodes: 62, | |
duration: "47м", | |
link: "https://hdrezka.me/series/drama/breaking-bad.html", | |
trailer: "https://www.youtube.com/embed/HhesaQXLuRY", | |
cast: ["Брайан Крэнстон", "Аарон Пол", "Анна Ганн"], | |
director: "Винс Гиллиган", | |
studio: "AMC" | |
}, | |
{ | |
id: 5, | |
title: "Дюна", | |
type: "movie", | |
year: 2021, | |
rating: 8.0, | |
genre: "Фантастика", | |
poster: "https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=300&h=450&fit=crop", | |
description: "Наследник знаменитого дома Атрейдесов Пол отправляется вместе с семьёй на самую опасную планету во Вселенной — Арракис, чтобы обеспечить будущее своего рода.", | |
duration: "2ч 35м", | |
link: "https://hdrezka.me/films/fiction/dune-2021.html", | |
trailer: "https://www.youtube.com/embed/n9xhJrPXop4", | |
cast: ["Тимоти Шаламе", "Ребекка Фергюсон", "Оскар Айзек"], | |
director: "Дени Вильнёв", | |
studio: "Warner Bros." | |
}, | |
{ | |
id: 6, | |
title: "Атака титанов", | |
type: "anime", | |
year: 2013, | |
rating: 9.0, | |
genre: "Экшен", | |
poster: "https://images.unsplash.com/photo-1578662015295-4d45d3c7c16d?w=300&h=450&fit=crop", | |
description: "Человечество живёт в городах, окружённых огромными стенами, защищающими от титанов — гигантских гуманоидных существ, пожирающих людей.", | |
episodes: 87, | |
seasons: 4, | |
duration: "24м", | |
link: "https://hdrezka.me/animation/drama/attack-on-titan.html", | |
trailer: "https://www.youtube.com/embed/LHtdKWJdif4", | |
cast: ["Юки Кадзи", "Марина Иноуэ", "Есимаса Хосоя"], | |
director: "Тэцуро Араки", | |
studio: "Wit Studio" | |
} | |
]; | |
}; | |
// Функція для отримання відео URL | |
const getVideoUrl = async (movieLink) => { | |
try { | |
// Імітація завантаження плеєра | |
await new Promise(resolve => setTimeout(resolve, 2000)); | |
return `https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1`; | |
} catch (error) { | |
console.error('Помилка отримання відео:', error); | |
return null; | |
} | |
}; | |
// Завантаження контенту при першому відкритті | |
useEffect(() => { | |
loadContent(); | |
// Завантаження збережених даних | |
const savedBookmarks = JSON.parse(localStorage.getItem('hdrezka_bookmarks') || '[]'); | |
const savedHistory = JSON.parse(localStorage.getItem('hdrezka_history') || '[]'); | |
setBookmarks(savedBookmarks); | |
setWatchHistory(savedHistory); | |
}, []); | |
// Збереження даних | |
useEffect(() => { | |
localStorage.setItem('hdrezka_bookmarks', JSON.stringify(bookmarks)); | |
}, [bookmarks]); | |
useEffect(() => { | |
localStorage.setItem('hdrezka_history', JSON.stringify(watchHistory)); | |
}, [watchHistory]); | |
const loadContent = async () => { | |
const content = await parseHDRezkaContent(); | |
setMovies(content); | |
}; | |
// Пошук з дебаунсом | |
useEffect(() => { | |
const searchTimeout = setTimeout(() => { | |
if (searchQuery.length > 0) { | |
searchMovies(); | |
} else { | |
// Якщо пошук порожній, показуємо всі фільми без loading | |
const demoMovies = getDemoMovies(); | |
setMovies(demoMovies); | |
} | |
}, 500); // Збільшуємо debounce до 500ms | |
return () => clearTimeout(searchTimeout); | |
}, [searchQuery, selectedCategory, sortBy]); | |
// const searchMovies = async () => { | |
// const results = await parseHDRezkaContent(searchQuery, selectedCategory); | |
// setMovies(results); | |
// }; | |
const searchMovies = async () => { | |
// Не показуємо loading для коротких пошукових запитів | |
if (searchQuery.length > 2) { | |
setIsLoading(true); | |
} | |
try { | |
// Симуляція запиту (зменшуємо час очікування) | |
await new Promise(resolve => setTimeout(resolve, 300)); | |
const demoMovies = getDemoMovies(); | |
let filteredMovies = demoMovies; | |
if (searchQuery) { | |
filteredMovies = demoMovies.filter(movie => | |
movie.title.toLowerCase().includes(searchQuery.toLowerCase()) || | |
movie.description.toLowerCase().includes(searchQuery.toLowerCase()) | |
); | |
} | |
if (selectedCategory !== 'all') { | |
filteredMovies = filteredMovies.filter(movie => movie.type === selectedCategory); | |
} | |
// Сортування | |
filteredMovies.sort((a, b) => { | |
switch (sortBy) { | |
case 'rating': | |
return b.rating - a.rating; | |
case 'year': | |
return b.year - a.year; | |
case 'title': | |
return a.title.localeCompare(b.title); | |
default: | |
return 0; | |
} | |
}); | |
setMovies(filteredMovies); | |
} catch (error) { | |
console.error('Помилка пошуку:', error); | |
setMovies(getDemoMovies()); | |
} finally { | |
setIsLoading(false); | |
} | |
}; | |
const categories = [ | |
{ id: 'all', name: 'Все', icon: Home }, | |
{ id: 'movie', name: 'Фильмы', icon: Film }, | |
{ id: 'series', name: 'Сериалы', icon: Tv }, | |
{ id: 'anime', name: 'Аниме', icon: Star } | |
]; | |
const genres = [ | |
{ id: 'all', name: 'Все жанры' }, | |
{ id: 'action', name: 'Экшен' }, | |
{ id: 'drama', name: 'Драма' }, | |
{ id: 'comedy', name: 'Комедия' }, | |
{ id: 'horror', name: 'Ужасы' }, | |
{ id: 'sci-fi', name: 'Фантастика' }, | |
{ id: 'romance', name: 'Романтика' } | |
]; | |
const years = [ | |
{ id: 'all', name: 'Все годы' }, | |
{ id: '2024', name: '2024' }, | |
{ id: '2023', name: '2023' }, | |
{ id: '2022', name: '2022' }, | |
{ id: '2021', name: '2021' }, | |
{ id: '2020', name: '2020' } | |
]; | |
const sortOptions = [ | |
{ id: 'rating', name: 'По рейтингу' }, | |
{ id: 'year', name: 'По году' }, | |
{ id: 'title', name: 'По названию' } | |
]; | |
const filteredMovies = movies.filter(movie => { | |
const matchesCategory = selectedCategory === 'all' || movie.type === selectedCategory; | |
const matchesSearch = movie.title.toLowerCase().includes(searchQuery.toLowerCase()); | |
const matchesYear = selectedYear === 'all' || movie.year.toString() === selectedYear; | |
const matchesGenre = selectedGenre === 'all' || movie.genre.toLowerCase().includes(selectedGenre.toLowerCase()); | |
return matchesCategory && matchesSearch && matchesYear && matchesGenre; | |
}); | |
const toggleBookmark = (movie) => { | |
setBookmarks(prev => { | |
const isBookmarked = prev.find(b => b.id === movie.id); | |
if (isBookmarked) { | |
return prev.filter(b => b.id !== movie.id); | |
} else { | |
return [...prev, movie]; | |
} | |
}); | |
}; | |
const addToHistory = (movie) => { | |
setWatchHistory(prev => { | |
const filtered = prev.filter(h => h.id !== movie.id); | |
return [{ ...movie, watchedAt: new Date().toISOString() }, ...filtered].slice(0, 50); | |
}); | |
}; | |
const isBookmarked = (movieId) => { | |
return bookmarks.some(b => b.id === movieId); | |
}; | |
const playMovie = async (movie) => { | |
addToHistory(movie); | |
const videoUrl = await getVideoUrl(movie.link); | |
if (videoUrl) { | |
setCurrentVideoUrl(videoUrl); | |
setShowPlayer(true); | |
} | |
}; | |
// Компонент банеру встановлення | |
const InstallBanner = () => { | |
if (!showInstallBanner) return null; | |
return ( | |
<div className="bg-gradient-to-r from-red-600 to-red-700 text-white p-4 flex items-center justify-between"> | |
<div className="flex items-center gap-3"> | |
<Download className="h-5 w-5" /> | |
<div> | |
<p className="font-semibold">Установить приложение</p> | |
<p className="text-sm opacity-90">Быстрый доступ к HDRezka</p> | |
</div> | |
</div> | |
<div className="flex gap-2"> | |
<button | |
onClick={installApp} | |
className="bg-white text-red-600 px-4 py-2 rounded font-semibold text-sm hover:bg-gray-100" | |
> | |
Установить | |
</button> | |
<button | |
onClick={() => setShowInstallBanner(false)} | |
className="p-2 hover:bg-red-800 rounded" | |
> | |
<X className="h-4 w-4" /> | |
</button> | |
</div> | |
</div> | |
); | |
}; | |
// Компонент фільтрів | |
const FilterPanel = () => { | |
if (!filterOpen) return null; | |
return ( | |
<div className="bg-gray-800 p-4 border-t border-gray-700"> | |
<div className="space-y-4"> | |
<div> | |
<label className="block text-sm font-medium mb-2">Сортировка</label> | |
<select | |
value={sortBy} | |
onChange={(e) => setSortBy(e.target.value)} | |
className="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" | |
> | |
{sortOptions.map(option => ( | |
<option key={option.id} value={option.id}>{option.name}</option> | |
))} | |
</select> | |
</div> | |
<div> | |
<label className="block text-sm font-medium mb-2">Жанр</label> | |
<select | |
value={selectedGenre} | |
onChange={(e) => setSelectedGenre(e.target.value)} | |
className="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" | |
> | |
{genres.map(genre => ( | |
<option key={genre.id} value={genre.id}>{genre.name}</option> | |
))} | |
</select> | |
</div> | |
<div> | |
<label className="block text-sm font-medium mb-2">Год</label> | |
<select | |
value={selectedYear} | |
onChange={(e) => setSelectedYear(e.target.value)} | |
className="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" | |
> | |
{years.map(year => ( | |
<option key={year.id} value={year.id}>{year.name}</option> | |
))} | |
</select> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
// Компонент статусу мережі | |
const NetworkStatus = () => { | |
if (isOnline) return null; | |
return ( | |
<div className="bg-yellow-600 text-white p-2 text-center text-sm"> | |
Нет подключения к интернету | |
</div> | |
); | |
}; | |
// Компонент відеоплеєра | |
const VideoPlayer = ({ videoUrl, movie, onClose }) => ( | |
<div className="fixed inset-0 bg-black z-50 flex flex-col"> | |
<div className="bg-gray-800 p-4 flex items-center justify-between"> | |
<h2 className="text-white font-semibold truncate">{movie.title}</h2> | |
<button | |
onClick={onClose} | |
className="text-white hover:text-gray-300 p-2" | |
> | |
<X className="h-6 w-6" /> | |
</button> | |
</div> | |
<div className="flex-1 bg-black flex items-center justify-center"> | |
<iframe | |
src={videoUrl} | |
className="w-full h-full" | |
allowFullScreen | |
frameBorder="0" | |
allow="autoplay; fullscreen" | |
/> | |
</div> | |
</div> | |
); | |
const MovieCard = ({ movie }) => ( | |
<div | |
className={`bg-gray-800 rounded-lg overflow-hidden shadow-lg cursor-pointer transform transition-all duration-300 hover:scale-105 hover:shadow-2xl ${ | |
viewMode === 'list' ? 'flex flex-row h-32' : 'flex flex-col' | |
}`} | |
onClick={() => { | |
setSelectedMovie(movie); | |
setCurrentPage('movie'); | |
}} | |
> | |
<div className={`relative ${viewMode === 'list' ? 'w-24 flex-shrink-0' : 'w-full'}`}> | |
<img | |
src={movie.poster} | |
alt={movie.title} | |
className={`object-cover ${viewMode === 'list' ? 'w-full h-full' : 'w-full h-48'}`} | |
/> | |
<div className="absolute top-2 right-2 bg-black bg-opacity-70 text-white px-2 py-1 rounded text-xs"> | |
⭐ {movie.rating} | |
</div> | |
<button | |
onClick={(e) => { | |
e.stopPropagation(); | |
toggleBookmark(movie); | |
}} | |
className={`absolute top-2 left-2 p-1 rounded-full ${ | |
isBookmarked(movie.id) ? 'bg-red-500' : 'bg-black bg-opacity-50' | |
} text-white hover:bg-red-600 transition-colors`} | |
> | |
<Bookmark className="h-3 w-3" /> | |
</button> | |
</div> | |
<div className={`p-3 ${viewMode === 'list' ? 'flex-1' : ''}`}> | |
<h3 className={`text-white font-semibold mb-1 ${viewMode === 'list' ? 'text-sm' : 'text-base'} line-clamp-2`}> | |
{movie.title} | |
</h3> | |
<div className={`flex items-center justify-between text-gray-400 ${viewMode === 'list' ? 'text-xs' : 'text-sm'}`}> | |
<span>{movie.year}</span> | |
<span className="capitalize">{movie.type}</span> | |
</div> | |
{movie.duration && ( | |
<div className={`text-gray-400 ${viewMode === 'list' ? 'text-xs' : 'text-sm'} mt-1`}> | |
{movie.duration} | |
</div> | |
)} | |
{movie.seasons && ( | |
<div className={`text-gray-400 ${viewMode === 'list' ? 'text-xs' : 'text-sm'} mt-1`}> | |
{movie.seasons} сезон{movie.seasons > 1 ? (movie.seasons > 4 ? 'ов' : 'а') : ''} • {movie.episodes} серий | |
</div> | |
)} | |
</div> | |
</div> | |
); | |
const MovieDetail = ({ movie }) => ( | |
<div className="min-h-screen bg-gray-900 text-white"> | |
<div className="relative"> | |
<img | |
src={movie.poster} | |
alt={movie.title} | |
className="w-full h-64 object-cover" | |
/> | |
<div className="absolute inset-0 bg-gradient-to-t from-gray-900 via-transparent to-transparent" /> | |
<button | |
onClick={() => { | |
setCurrentPage('home'); | |
setSelectedMovie(null); | |
}} | |
className="absolute top-4 left-4 p-2 bg-black bg-opacity-50 rounded-full text-white hover:bg-opacity-70 transition-colors" | |
> | |
<ArrowLeft className="h-6 w-6" /> | |
</button> | |
<button | |
onClick={() => toggleBookmark(movie)} | |
className={`absolute top-4 right-4 p-2 rounded-full ${ | |
isBookmarked(movie.id) ? 'bg-red-500' : 'bg-black bg-opacity-50' | |
} text-white hover:bg-red-600 transition-colors`} | |
> | |
<Bookmark className="h-6 w-6" /> | |
</button> | |
</div> | |
<div className="p-6"> | |
<h1 className="text-2xl font-bold mb-2">{movie.title}</h1> | |
<div className="flex items-center gap-4 mb-4 text-gray-400"> | |
<span>{movie.year}</span> | |
<span>⭐ {movie.rating}</span> | |
<span className="capitalize">{movie.type}</span> | |
<span>{movie.genre}</span> | |
</div> | |
<div className="flex gap-3 mb-6"> | |
<button | |
onClick={() => playMovie(movie)} | |
className="flex-1 bg-red-600 hover:bg-red-700 text-white font-semibold py-3 px-6 rounded-lg flex items-center justify-center gap-2 transition-colors" | |
> | |
<Play className="h-5 w-5" /> | |
Смотреть | |
</button> | |
{movie.trailer && ( | |
<button | |
onClick={() => window.open(movie.trailer, '_blank')} | |
className="bg-gray-700 hover:bg-gray-600 text-white font-semibold py-3 px-6 rounded-lg flex items-center justify-center gap-2 transition-colors" | |
> | |
<Play className="h-5 w-5" /> | |
Трейлер | |
</button> | |
)} | |
</div> | |
<div className="mb-6"> | |
<h3 className="text-lg font-semibold mb-3">Описание</h3> | |
<p className="text-gray-300 leading-relaxed">{movie.description}</p> | |
</div> | |
<div className="mb-6"> | |
<h3 className="text-lg font-semibold mb-3">Информация</h3> | |
<div className="space-y-2 text-gray-300"> | |
<div><span className="text-gray-400">Режиссёр:</span> {movie.director}</div> | |
<div><span className="text-gray-400">Студия:</span> {movie.studio}</div> | |
<div><span className="text-gray-400">В главных ролях:</span> {movie.cast?.join(', ')}</div> | |
{movie.duration && ( | |
<div><span className="text-gray-400">Длительность:</span> {movie.duration}</div> | |
)} | |
</div> | |
</div> | |
{movie.seasons && ( | |
<div className="mb-6"> | |
<h3 className="text-lg font-semibold mb-3">Сезоны</h3> | |
<div className="grid grid-cols-2 gap-2"> | |
{Array.from({ length: movie.seasons }, (_, i) => ( | |
<button | |
key={i} | |
className="bg-gray-700 hover:bg-gray-600 text-white py-2 px-4 rounded transition-colors" | |
> | |
Сезон {i + 1} | |
</button> | |
))} | |
</div> | |
</div> | |
)} | |
</div> | |
</div> | |
); | |
const HomePage = () => ( | |
<div className="min-h-screen bg-gray-900 text-white"> | |
<NetworkStatus /> | |
<InstallBanner /> | |
{/* Header */} | |
<div className="bg-gray-800 p-4"> | |
<div className="flex items-center justify-between mb-4"> | |
<h1 className="text-xl font-bold">HDRezka Mobile</h1> | |
<div className="flex items-center gap-2"> | |
<button | |
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')} | |
className="p-2 hover:bg-gray-700 rounded-lg transition-colors" | |
> | |
{viewMode === 'grid' ? <List className="h-5 w-5" /> : <Grid className="h-5 w-5" />} | |
</button> | |
<button | |
onClick={() => setIsMenuOpen(!isMenuOpen)} | |
className="p-2 hover:bg-gray-700 rounded-lg transition-colors" | |
> | |
{isMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />} | |
</button> | |
</div> | |
</div> | |
{/* Search */} | |
<div className="relative mb-4"> | |
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" /> | |
<input | |
ref={searchInputRef} | |
type="text" | |
placeholder="Поиск фильмов и сериалов..." | |
value={searchQuery} | |
onChange={(e) => setSearchQuery(e.target.value)} | |
className="w-full pl-10 pr-4 py-3 bg-gray-700 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:outline-none" | |
/> | |
</div> | |
{/* Filter Toggle */} | |
<div className="flex items-center justify-between"> | |
<div className="flex gap-2 overflow-x-auto"> | |
{categories.map(category => { | |
const Icon = category.icon; | |
return ( | |
<button | |
key={category.id} | |
onClick={() => setSelectedCategory(category.id)} | |
className={`flex items-center gap-2 px-3 py-2 rounded-lg whitespace-nowrap transition-colors ${ | |
selectedCategory === category.id | |
? 'bg-red-600 text-white' | |
: 'bg-gray-700 text-gray-300 hover:bg-gray-600' | |
}`} | |
> | |
<Icon className="h-4 w-4" /> | |
<span className="text-sm">{category.name}</span> | |
</button> | |
); | |
})} | |
</div> | |
<button | |
onClick={() => setFilterOpen(!filterOpen)} | |
className="p-2 hover:bg-gray-700 rounded-lg transition-colors" | |
> | |
<Filter className="h-5 w-5" /> | |
</button> | |
</div> | |
</div> | |
{/* Mobile Menu */} | |
{isMenuOpen && ( | |
<div className="bg-gray-800 p-4 border-t border-gray-700"> | |
<div className="space-y-2"> | |
<button | |
onClick={() => { | |
setCurrentPage('bookmarks'); | |
setIsMenuOpen(false); | |
}} | |
className="w-full flex items-center gap-2 p-3 bg-gray-700 text-gray-300 hover:bg-gray-600 rounded-lg transition-colors" | |
> | |
<Bookmark className="h-5 w-5" /> | |
<span>Закладки ({bookmarks.length})</span> | |
</button> | |
<button | |
onClick={() => { | |
setCurrentPage('history'); | |
setIsMenuOpen(false); | |
}} | |
className="w-full flex items-center gap-2 p-3 bg-gray-700 text-gray-300 hover:bg-gray-600 rounded-lg transition-colors" | |
> | |
<Clock className="h-5 w-5" /> | |
<span>История ({watchHistory.length})</span> | |
</button> | |
<button | |
onClick={() => { | |
setCurrentPage('settings'); | |
setIsMenuOpen(false); | |
}} | |
className="w-full flex items-center gap-2 p-3 bg-gray-700 text-gray-300 hover:bg-gray-600 rounded-lg transition-colors" | |
> | |
<Settings className="h-5 w-5" /> | |
<span>Настройки</span> | |
</button> | |
</div> | |
</div> | |
)} | |
<FilterPanel /> | |
{/* Content */} | |
<div className="p-4"> | |
{isLoading ? ( | |
<div className="flex items-center justify-center py-12"> | |
<Loader className="h-8 w-8 animate-spin text-red-500" /> | |
</div> | |
) : ( | |
<> | |
<div className="flex items-center justify-between mb-4"> | |
<h2 className="text-lg font-semibold"> | |
{selectedCategory === 'all' ? 'Популярные' : categories.find(c => c.id === selectedCategory)?.name} | |
</h2> | |
<span className="text-gray-400 text-sm"> | |
{filteredMovies.length} результат{filteredMovies.length !== 1 ? 'ов' : ''} | |
</span> | |
</div> | |
<div className={`${viewMode === 'grid' ? 'grid grid-cols-2 gap-4' : 'space-y-4'}`}> | |
{filteredMovies.map(movie => ( | |
<MovieCard key={movie.id} movie={movie} /> | |
))} | |
</div> | |
{filteredMovies.length === 0 && !isLoading && ( | |
<div className="text-center py-12"> | |
<Film className="h-16 w-16 text-gray-600 mx-auto mb-4" /> | |
<p className="text-gray-400">Ничего не найдено</p> | |
</div> | |
)} | |
</> | |
)} | |
</div> | |
</div> | |
); | |
const BookmarksPage = () => ( | |
<div className="min-h-screen bg-gray-900 text-white"> | |
<div className="bg-gray-800 p-4 flex items-center gap-4"> | |
<button | |
onClick={() => setCurrentPage('home')} | |
className="p-2 hover:bg-gray-700 rounded-lg transition-colors" | |
> | |
<ArrowLeft className="h-6 w-6" /> | |
</button> | |
<h1 className="text-xl font-bold">Закладки</h1> | |
</div> | |
<div className="p-4"> | |
{bookmarks.length === 0 ? ( | |
<div className="text-center py-12"> | |
<Bookmark className="h-16 w-16 text-gray-600 mx-auto mb-4" /> | |
<p className="text-gray-400">Нет сохраненных фильмов</p> | |
</div> | |
) : ( | |
<div className="grid grid-cols-2 gap-4"> | |
{bookmarks.map(movie => ( | |
<MovieCard key={movie.id} movie={movie} /> | |
))} | |
</div> | |
)} | |
</div> | |
</div> | |
); | |
const HistoryPage = () => ( | |
<div className="min-h-screen bg-gray-900 text-white"> | |
<div className="bg-gray-800 p-4 flex items-center gap-4"> | |
<button | |
onClick={() => setCurrentPage('home')} | |
className="p-2 hover:bg-gray-700 rounded-lg transition-colors" | |
> | |
<ArrowLeft className="h-6 w-6" /> | |
</button> | |
<h1 className="text-xl font-bold">История просмотров</h1> | |
</div> | |
<div className="p-4"> | |
{watchHistory.length === 0 ? ( | |
<div className="text-center py-12"> | |
<Clock className="h-16 w-16 text-gray-600 mx-auto mb-4" /> | |
<p className="text-gray-400">История просмотров пуста</p> | |
</div> | |
) : ( | |
<div className="space-y-4"> | |
{watchHistory.map(movie => ( | |
<div key={movie.id} className="flex items-center gap-4 bg-gray-800 p-4 rounded-lg"> | |
<img | |
src={movie.poster} | |
alt={movie.title} | |
className="w-16 h-24 object-cover rounded" | |
/> | |
<div className="flex-1"> | |
<h3 className="font-semibold mb-1">{movie.title}</h3> | |
<p className="text-gray-400 text-sm">{movie.year} • {movie.type}</p> | |
<p className="text-gray-500 text-xs mt-1"> | |
Просмотрено: {new Date(movie.watchedAt).toLocaleDateString()} | |
</p> | |
</div> | |
<button | |
onClick={() => playMovie(movie)} | |
className="p-2 bg-red-600 hover:bg-red-700 rounded-lg transition-colors" | |
> | |
<Play className="h-5 w-5" /> | |
</button> | |
</div> | |
))} | |
</div> | |
)} | |
</div> | |
</div> | |
); | |
const SettingsPage = () => ( | |
<div className="min-h-screen bg-gray-900 text-white"> | |
<div className="bg-gray-800 p-4 flex items-center gap-4"> | |
<button | |
onClick={() => setCurrentPage('home')} | |
className="p-2 hover:bg-gray-700 rounded-lg transition-colors" | |
> | |
<ArrowLeft className="h-6 w-6" /> | |
</button> | |
<h1 className="text-xl font-bold">Настройки</h1> | |
</div> | |
<div className="p-4 space-y-4"> | |
<div className="bg-gray-800 p-4 rounded-lg"> | |
<h3 className="font-semibold mb-2">Приложение</h3> | |
<div className="space-y-2 text-sm text-gray-300"> | |
<p>Версия: 1.0.0</p> | |
<p>Статус: {isOnline ? 'Онлайн' : 'Оффлайн'}</p> | |
<p>Закладки: {bookmarks.length}</p> | |
<p>История: {watchHistory.length}</p> | |
</div> | |
</div> | |
<div className="bg-gray-800 p-4 rounded-lg"> | |
<h3 className="font-semibold mb-2">Действия</h3> | |
<div className="space-y-2"> | |
<button | |
onClick={() => { | |
setBookmarks([]); | |
alert('Закладки очищены'); | |
}} | |
className="w-full text-left p-2 text-red-400 hover:bg-gray-700 rounded" | |
> | |
Очистить закладки | |
</button> | |
<button | |
onClick={() => { | |
setWatchHistory([]); | |
alert('История очищена'); | |
}} | |
className="w-full text-left p-2 text-red-400 hover:bg-gray-700 rounded" | |
> | |
Очистить историю | |
</button> | |
</div> | |
</div> | |
<div className="bg-gray-800 p-4 rounded-lg"> | |
<h3 className="font-semibold mb-2">О приложении</h3> | |
<p className="text-sm text-gray-300"> | |
HDRezka Mobile - неофициальный клиент для просмотра фильмов и сериалов. | |
Все материалы принадлежат их правообладателям. | |
</p> | |
</div> | |
</div> | |
</div> | |
); | |
return ( | |
<div className="max-w-md mx-auto bg-gray-900 min-h-screen"> | |
{showPlayer && currentVideoUrl && selectedMovie && ( | |
<VideoPlayer | |
videoUrl={currentVideoUrl} | |
movie={selectedMovie} | |
onClose={() => { | |
setShowPlayer(false); | |
setCurrentVideoUrl(null); | |
}} | |
/> | |
)} | |
{currentPage === 'home' && <HomePage />} | |
{currentPage === 'bookmarks' && <BookmarksPage />} | |
{currentPage === 'history' && <HistoryPage />} | |
{currentPage === 'settings' && <SettingsPage />} | |
{currentPage === 'movie' && selectedMovie && <MovieDetail movie={selectedMovie} />} | |
</div> | |
); | |
}; | |
export default HDRezkaMobile; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment