mirror of
https://gitlab.com/foxixus/neomovies.git
synced 2025-10-29 10:28:49 +05:00
Add categories
This commit is contained in:
136
src/components/CategoryCard.tsx
Normal file
136
src/components/CategoryCard.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import styled from 'styled-components';
|
||||
import { Category } from '@/lib/api';
|
||||
|
||||
interface CategoryCardProps {
|
||||
category: Category;
|
||||
backgroundUrl?: string;
|
||||
}
|
||||
|
||||
// Словарь цветов для разных жанров
|
||||
const genreColors: Record<number, string> = {
|
||||
28: '#E53935', // Боевик - красный
|
||||
12: '#43A047', // Приключения - зеленый
|
||||
16: '#FB8C00', // Мультфильм - оранжевый
|
||||
35: '#FFEE58', // Комедия - желтый
|
||||
80: '#424242', // Криминал - темно-серый
|
||||
99: '#8D6E63', // Документальный - коричневый
|
||||
18: '#5E35B1', // Драма - пурпурный
|
||||
10751: '#EC407A', // Семейный - розовый
|
||||
14: '#7E57C2', // Фэнтези - фиолетовый
|
||||
36: '#795548', // История - коричневый
|
||||
27: '#212121', // Ужасы - черный
|
||||
10402: '#26A69A', // Музыка - бирюзовый
|
||||
9648: '#5C6BC0', // Детектив - индиго
|
||||
10749: '#EC407A', // Мелодрама - розовый
|
||||
878: '#00BCD4', // Фантастика - голубой
|
||||
10770: '#9E9E9E', // ТВ фильм - серый
|
||||
53: '#FFA000', // Триллер - янтарный
|
||||
10752: '#455A64', // Военный - сине-серый
|
||||
37: '#8D6E63', // Вестерн - коричневый
|
||||
// Добавим цвета для популярных жанров сериалов
|
||||
10759: '#1E88E5', // Боевик и приключения - синий
|
||||
10762: '#00ACC1', // Детский - циан
|
||||
10763: '#546E7A', // Новости - сине-серый
|
||||
10764: '#F06292', // Реалити-шоу - розовый
|
||||
10765: '#00BCD4', // Фантастика и фэнтези - голубой
|
||||
10766: '#5E35B1', // Мыльная опера - пурпурный
|
||||
10767: '#4CAF50', // Ток-шоу - зеленый
|
||||
10768: '#FFD54F' // Война и политика - желтый
|
||||
};
|
||||
|
||||
// Получаем цвет для категории или используем запасной вариант
|
||||
function getCategoryColor(categoryId: number): string {
|
||||
return genreColors[categoryId] || '#3949AB'; // Индиго как запасной вариант
|
||||
}
|
||||
|
||||
const CardContainer = styled.div<{ $bgUrl: string; $bgColor: string }>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
background-image: url(${props => props.$bgUrl || '/images/placeholder.jpg'});
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: ${props => props.$bgColor};
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&::after {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CardContent = styled.div`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
color: white;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const CategoryName = styled.h3`
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
`;
|
||||
|
||||
const CategoryCount = styled.p`
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.9;
|
||||
margin: 0.5rem 0 0;
|
||||
`;
|
||||
|
||||
function CategoryCard({ category, backgroundUrl }: CategoryCardProps) {
|
||||
const router = useRouter();
|
||||
const [imageUrl, setImageUrl] = useState<string>(backgroundUrl || '/images/placeholder.jpg');
|
||||
|
||||
const categoryColor = getCategoryColor(category.id);
|
||||
|
||||
function handleClick() {
|
||||
router.push(`/categories/${category.id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
$bgUrl={imageUrl}
|
||||
$bgColor={categoryColor}
|
||||
onClick={handleClick}
|
||||
role="button"
|
||||
aria-label={`Категория ${category.name}`}
|
||||
>
|
||||
<CardContent>
|
||||
<CategoryName>{category.name}</CategoryName>
|
||||
<CategoryCount>Фильмы и сериалы</CategoryCount>
|
||||
</CardContent>
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default CategoryCard;
|
||||
@@ -1,17 +1,33 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const GlassCard = styled.div`
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(20px);
|
||||
background: rgba(0, 0, 0, 0.65); /* Увеличили непрозрачность фона для лучшей читаемости */
|
||||
/* Убираем тяжелый blur на мобильных устройствах */
|
||||
@supports (backdrop-filter: blur(20px)) {
|
||||
backdrop-filter: blur(20px);
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 3rem;
|
||||
border-radius: 24px;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px; /* Уменьшили радиус для компактности */
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
box-shadow:
|
||||
0 8px 32px 0 rgba(0, 0, 0, 0.3),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
margin: 0 auto;
|
||||
overflow: hidden; /* Предотвращаем выход контента за пределы карточки */
|
||||
|
||||
@media (max-width: 640px) {
|
||||
padding: 1.75rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
padding: 1.5rem 1.25rem;
|
||||
margin: 0 0.5rem;
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
`;
|
||||
|
||||
export default GlassCard;
|
||||
|
||||
@@ -4,65 +4,97 @@ import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import styled from 'styled-components';
|
||||
import { Movie } from '@/types/movie';
|
||||
import { Movie, TVShow } from '@/types/movie';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
import { useImageLoader } from '@/hooks/useImageLoader';
|
||||
|
||||
// Тип-гард для проверки, является ли объект сериалом
|
||||
function isTVShow(media: Movie | TVShow): media is TVShow {
|
||||
return 'name' in media && 'first_air_date' in media;
|
||||
}
|
||||
|
||||
interface MovieCardProps {
|
||||
movie: Movie;
|
||||
movie: Movie | TVShow;
|
||||
priority?: boolean;
|
||||
}
|
||||
|
||||
export default function MovieCard({ movie, priority = false }: MovieCardProps) {
|
||||
const { imageUrl, isLoading } = useImageLoader(movie.poster_path, 'w342');
|
||||
// Определяем, это фильм или сериал с помощью тип-гарда
|
||||
const isTV = isTVShow(movie);
|
||||
|
||||
// Используем правильный заголовок и дату в зависимости от типа
|
||||
const title = isTV ? movie.name || 'Без названия' : movie.title || 'Без названия';
|
||||
const date = isTV ? movie.first_air_date : movie.release_date;
|
||||
|
||||
// Выбираем правильный URL
|
||||
const url = isTV ? `/tv/${movie.id}` : `/movie/${movie.id}`;
|
||||
|
||||
// Загружаем изображение с оптимизированным размером для конкретного устройства
|
||||
// Используем меньший размер изображения для мобильных устройств
|
||||
const { imageUrl, isLoading } = useImageLoader(movie.poster_path, 'w342'); // Используем поддерживаемый размер
|
||||
|
||||
return (
|
||||
<Card href={`/movie/${movie.id}`}>
|
||||
<Card href={url}>
|
||||
<PosterWrapper>
|
||||
{isLoading ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-700">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-white" />
|
||||
</div>
|
||||
<LoadingPlaceholder aria-label="Загрузка постера">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-blue-500" />
|
||||
</LoadingPlaceholder>
|
||||
) : imageUrl ? (
|
||||
<Poster
|
||||
src={imageUrl}
|
||||
alt={movie.title}
|
||||
alt={`Постер ${title}`}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
sizes="(max-width: 640px) 150px, (max-width: 768px) 180px, (max-width: 1024px) 200px, 220px"
|
||||
priority={priority}
|
||||
className="object-cover transition-opacity duration-300 group-hover:opacity-75"
|
||||
loading={priority ? 'eager' : 'lazy'}
|
||||
className="object-cover"
|
||||
unoptimized // Отключаем оптимизацию Next.js, так как используем CDN
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-700 text-gray-400">
|
||||
No Image
|
||||
</div>
|
||||
<NoImagePlaceholder aria-label="Нет изображения">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</NoImagePlaceholder>
|
||||
)}
|
||||
<Rating style={{ backgroundColor: getRatingColor(movie.vote_average) }}>
|
||||
{movie.vote_average.toFixed(1)}
|
||||
</Rating>
|
||||
</PosterWrapper>
|
||||
<Content>
|
||||
<Title>{movie.title}</Title>
|
||||
<Year>{formatDate(movie.release_date)}</Year>
|
||||
<Title>{title}</Title>
|
||||
<Year>{date ? formatDate(date) : 'Без даты'}</Year>
|
||||
</Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Функция для определения цвета рейтинга
|
||||
const getRatingColor = (rating: number) => {
|
||||
if (rating >= 7) return '#4CAF50';
|
||||
if (rating >= 5) return '#FFC107';
|
||||
return '#F44336';
|
||||
};
|
||||
|
||||
// Оптимизированные стилевые компоненты для мобильных устройств
|
||||
const Card = styled(Link)`
|
||||
position: relative;
|
||||
border-radius: 16px;
|
||||
border-radius: 12px; /* Уменьшили радиус для компактности */
|
||||
overflow: hidden;
|
||||
background: #242424;
|
||||
background: #1c1c1c; /* Темнее фон для лучшего контраста */
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
will-change: transform; /* Подсказка браузеру для оптимизации */
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex; /* Используем flexbox для лучшего контроля над высотой */
|
||||
flex-direction: column;
|
||||
height: 100%; /* Занимаем всю доступную высоту */
|
||||
|
||||
@media (max-width: 640px) {
|
||||
border-radius: 8px; /* Еще меньше радиус на малых экранах */
|
||||
}
|
||||
`;
|
||||
|
||||
const PosterWrapper = styled.div`
|
||||
@@ -75,33 +107,85 @@ const Poster = styled(Image)`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
// Плейсхолдер для загрузки
|
||||
const LoadingPlaceholder = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #2a2a2a;
|
||||
`;
|
||||
|
||||
// Плейсхолдер для отсутствующих изображений
|
||||
const NoImagePlaceholder = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #1c1c1c;
|
||||
color: #6b7280;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
padding: 12px;
|
||||
flex-grow: 1; /* Занимаем все оставшееся пространство */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
padding: 8px 10px; /* Уменьшенные отступы для мобильных устройств */
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.h3`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
margin: 0 0 4px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: normal; /* Важно: разрешаем перенос текста */
|
||||
max-height: 2.8em; /* Фиксированная высота для заголовка */
|
||||
|
||||
@media (max-width: 640px) {
|
||||
font-size: 13px; /* Уменьшенный размер шрифта для мобильных устройств */
|
||||
line-height: 1.3;
|
||||
}
|
||||
`;
|
||||
|
||||
const Year = styled.div`
|
||||
const Year = styled.p`
|
||||
font-size: 12px;
|
||||
color: #808191;
|
||||
margin-top: 4px;
|
||||
color: #aaa;
|
||||
margin: 0;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
font-size: 11px; /* Уменьшенный размер шрифта для мобильных устройств */
|
||||
}
|
||||
`;
|
||||
|
||||
const Rating = styled.div`
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 3px 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
z-index: 2;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
padding: 2px 5px;
|
||||
font-size: 11px;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -75,14 +75,15 @@ const MobileNav = styled.nav`
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(18, 18, 23, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
background: #121217; /* Заменили полупрозрачный фон на сплошной для производительности */
|
||||
/* Удалили тяжелый эффект blur для мобильных устройств */
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); /* Добавили тень для визуального разделения */
|
||||
z-index: 50;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
height: 56px; /* Уменьшили высоту для компактности */
|
||||
|
||||
@media (min-width: 769px) {
|
||||
display: none;
|
||||
@@ -115,17 +116,19 @@ const MobileMenuButton = styled.button`
|
||||
|
||||
const MobileMenu = styled.div<{ $isOpen: boolean }>`
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
top: 56px; /* Соответствует новой высоте навбара */
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(18, 18, 23, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
background: #121217; /* Сплошной фон без прозрачности */
|
||||
/* Удалили тяжелый эффект blur */
|
||||
transform: translateX(${props => props.$isOpen ? '0' : '100%'});
|
||||
transition: transform 0.3s ease-in-out;
|
||||
transition: transform 0.25s ease-out; /* Ускорили анимацию */
|
||||
padding: 1rem;
|
||||
z-index: 49;
|
||||
overflow-y: auto;
|
||||
will-change: transform; /* Подсказка браузеру для оптимизации */
|
||||
-webkit-overflow-scrolling: touch; /* Плавный скролл на iOS */
|
||||
|
||||
@media (min-width: 769px) {
|
||||
display: none;
|
||||
@@ -136,20 +139,27 @@ const MobileMenuItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
padding: 1rem 0.75rem; /* Уменьшили горизонтальные отступы */
|
||||
margin-bottom: 0.25rem; /* Добавили отступ между элементами */
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 12px;
|
||||
transition: background-color 0.2s;
|
||||
border-radius: 8px; /* Уменьшили радиус для компактности */
|
||||
font-size: 1rem;
|
||||
font-weight: 500; /* Добавили небольшое утолщение шрифта */
|
||||
position: relative; /* Для анимации ripple-эффекта */
|
||||
overflow: hidden; /* Для анимации ripple-эффекта */
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
/* Заменили плавную анимацию на мгновенную для мобильных устройств */
|
||||
&:active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
transform: scale(0.98); /* Небольшой эффект нажатия */
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 22px; /* Увеличили иконки для лучшей видимости на мобильных устройствах */
|
||||
height: 22px;
|
||||
min-width: 22px; /* Чтобы иконки были выровнены */
|
||||
color: #3b82f6; /* Цвет для лучшего визуального разделения */
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user