full change ui and small fixes

This commit is contained in:
2025-07-08 00:15:55 +03:00
parent 4aad0c8d48
commit bc2a4a623f
42 changed files with 10832 additions and 3337 deletions

View File

@@ -1,234 +1,14 @@
'use client';
import { useState, useEffect } from 'react';
import styled from 'styled-components';
import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import { moviesAPI } from '@/lib/neoApi';
import { getImageUrl } from '@/lib/neoApi';
import type { MovieDetails } from '@/lib/api';
import { useSettings } from '@/hooks/useSettings';
import MoviePlayer from '@/components/MoviePlayer';
import FavoriteButton from '@/components/FavoriteButton';
import { formatDate } from '@/lib/utils';
declare global {
interface Window {
kbox: any;
}
}
const Container = styled.div`
width: 100%;
min-height: 100vh;
padding: 0 24px;
`;
const Content = styled.div`
width: 100%;
max-width: 1200px;
margin: 0 auto;
@media (max-width: 768px) {
padding-top: 1rem;
}
`;
const MovieInfo = styled.div`
display: flex;
gap: 30px;
margin-bottom: 1rem;
@media (max-width: 768px) {
flex-direction: column;
gap: 1.5rem;
}
`;
const PosterContainer = styled.div`
flex-shrink: 0;
position: relative;
@media (max-width: 768px) {
display: flex;
justify-content: center;
}
`;
const Poster = styled.img`
width: 300px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
@media (max-width: 768px) {
width: 200px;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
@media (max-width: 480px) {
width: 160px;
}
`;
const Details = styled.div`
flex: 1;
@media (max-width: 768px) {
padding: 0 0.5rem;
}
`;
const Title = styled.h1`
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
color: white;
@media (max-width: 768px) {
font-size: 1.75rem;
margin-bottom: 0.75rem;
text-align: center;
}
@media (max-width: 480px) {
font-size: 1.5rem;
}
`;
const Info = styled.div`
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1rem;
@media (max-width: 768px) {
justify-content: center;
gap: 0.75rem;
}
`;
const InfoItem = styled.span`
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;
@media (max-width: 480px) {
font-size: 0.8rem;
background: rgba(255, 255, 255, 0.05);
padding: 0.35rem 0.6rem;
border-radius: 4px;
}
`;
const GenreList = styled.div`
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
@media (max-width: 768px) {
justify-content: center;
}
`;
const Genre = styled.span`
background: rgba(255, 255, 255, 0.1);
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.8);
@media (max-width: 480px) {
font-size: 0.8rem;
padding: 0.2rem 0.6rem;
background: rgba(59, 130, 246, 0.15);
}
`;
const Tagline = styled.div`
font-style: italic;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 1rem;
@media (max-width: 768px) {
text-align: center;
font-size: 0.9rem;
}
`;
const Overview = styled.p`
color: rgba(255, 255, 255, 0.8);
line-height: 1.6;
@media (max-width: 768px) {
font-size: 0.95rem;
text-align: justify;
margin-bottom: 1.5rem;
}
@media (max-width: 480px) {
font-size: 0.9rem;
line-height: 1.5;
}
`;
const ActionButtons = styled.div`
display: flex;
gap: 1rem;
margin-top: 1.5rem;
@media (max-width: 768px) {
justify-content: center;
margin-top: 1rem;
}
`;
const WatchButton = styled.button`
background: #e50914;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
@media (max-width: 480px) {
padding: 0.6rem 1.2rem;
font-size: 0.95rem;
}
&:hover {
background: #f40612;
}
`;
const PlayerSection = styled.div`
margin-top: 2rem;
@media (max-width: 768px) {
margin-top: 1.5rem;
}
`;
const LoadingContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
color: rgba(255, 255, 255, 0.7);
`;
const ErrorContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
color: #ff4444;
`;
import { useParams } from 'next/navigation';
import { PlayCircle, ArrowLeft } from 'lucide-react';
interface MovieContentProps {
movieId: string;
@@ -236,9 +16,11 @@ interface MovieContentProps {
}
export default function MovieContent({ movieId, initialMovie }: MovieContentProps) {
const { settings } = useSettings();
const [movie] = useState<MovieDetails>(initialMovie);
const [imdbId, setImdbId] = useState<string | null>(null);
const [isPlayerFullscreen, setIsPlayerFullscreen] = useState(false);
const [isControlsVisible, setIsControlsVisible] = useState(false);
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const fetchImdbId = async () => {
@@ -254,62 +36,135 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp
fetchImdbId();
}, [movieId]);
const showControls = () => {
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
setIsControlsVisible(true);
controlsTimeoutRef.current = setTimeout(() => {
setIsControlsVisible(false);
}, 3000);
};
const handleOpenPlayer = () => {
setIsPlayerFullscreen(true);
showControls();
};
const handleClosePlayer = () => {
setIsPlayerFullscreen(false);
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
};
return (
<Container>
<Content>
<MovieInfo>
<PosterContainer>
<Poster
src={getImageUrl(movie.poster_path)}
alt={movie.title}
loading="eager"
/>
</PosterContainer>
<>
<div className="min-h-screen bg-background text-foreground px-4 py-6 md:px-6 lg:px-8">
<div className="w-full">
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
{/* Left Column: Poster */}
<div className="md:col-span-1">
<div className="sticky top-24 max-w-sm mx-auto md:max-w-none md:mx-0">
<div className="relative aspect-[2/3] w-full overflow-hidden rounded-lg shadow-lg">
<Image
src={getImageUrl(movie.poster_path, 'w500')}
alt={`Постер фильма ${movie.title}`}
fill
className="object-cover"
unoptimized
/>
</div>
</div>
</div>
<Details>
<Title>{movie.title}</Title>
<Info>
<InfoItem>Рейтинг: {movie.vote_average.toFixed(1)}</InfoItem>
<InfoItem>Длительность: {movie.runtime} мин.</InfoItem>
<InfoItem>Дата выхода: {formatDate(movie.release_date)}</InfoItem>
</Info>
<GenreList>
{movie.genres.map(genre => (
<Genre key={genre.id}>{genre.name}</Genre>
))}
</GenreList>
{movie.tagline && <Tagline>{movie.tagline}</Tagline>}
<Overview>{movie.overview}</Overview>
<ActionButtons>
{imdbId && (
<WatchButton
onClick={() => document.getElementById('movie-player')?.scrollIntoView({ behavior: 'smooth' })}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5.14V19.14L19 12.14L8 5.14Z" fill="currentColor" />
</svg>
Смотреть
</WatchButton>
{/* Middle Column: Details */}
<div className="md:col-span-2">
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">
{movie.title}
</h1>
{movie.tagline && (
<p className="mt-1 text-lg text-muted-foreground">{movie.tagline}</p>
)}
<FavoriteButton
mediaId={movie.id.toString()}
mediaType="movie"
title={movie.title}
posterPath={movie.poster_path}
/>
</ActionButtons>
</Details>
</MovieInfo>
{imdbId && (
<PlayerSection id="movie-player">
<MoviePlayer
imdbId={imdbId}
/>
</PlayerSection>
)}
</Content>
</Container>
<div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-2">
<span className="font-medium">Рейтинг: {movie.vote_average.toFixed(1)}</span>
<span className="text-muted-foreground">|</span>
<span className="text-muted-foreground">{movie.runtime} мин.</span>
<span className="text-muted-foreground">|</span>
<span className="text-muted-foreground">{formatDate(movie.release_date)}</span>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{movie.genres.map((genre) => (
<span key={genre.id} className="rounded-full bg-secondary text-secondary-foreground px-3 py-1 text-xs font-medium">
{genre.name}
</span>
))}
</div>
<div className="mt-6 space-y-4 text-base text-muted-foreground">
<p>{movie.overview}</p>
</div>
<div className="mt-8 flex items-center gap-4">
{/* Mobile-only Watch Button */}
{imdbId && (
<button
onClick={handleOpenPlayer}
className="md:hidden flex items-center justify-center gap-2 rounded-md bg-red-500 px-6 py-3 text-base font-semibold text-white shadow-sm hover:bg-red-600"
>
<PlayCircle size={20} />
<span>Смотреть</span>
</button>
)}
<FavoriteButton
mediaId={movie.id.toString()}
mediaType="movie"
title={movie.title}
posterPath={movie.poster_path}
className="!bg-secondary !text-secondary-foreground hover:!bg-secondary/80"
/>
</div>
{/* Desktop-only Embedded Player */}
{imdbId && (
<div id="movie-player" className="mt-10 hidden md:block rounded-lg bg-secondary/50 p-4 shadow-inner">
<MoviePlayer
id={movie.id.toString()}
title={movie.title}
poster={movie.poster_path || ''}
imdbId={imdbId}
/>
</div>
)}
</div>
</div>
</div>
</div>
{/* Fullscreen Player for Mobile */}
{isPlayerFullscreen && imdbId && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black"
onMouseMove={showControls}
onClick={showControls}
>
<MoviePlayer
id={movie.id.toString()}
title={movie.title}
poster={movie.poster_path || ''}
imdbId={imdbId}
/>
<button
onClick={handleClosePlayer}
className={`absolute top-1/2 left-4 -translate-y-1/2 z-50 rounded-full bg-black/50 p-2 text-white transition-opacity duration-300 hover:bg-black/75 ${isControlsVisible ? 'opacity-100' : 'opacity-0'}`}
aria-label="Назад"
>
<ArrowLeft size={24} />
</button>
</div>
)}
</>
);
}