Исправления на страницах

This commit is contained in:
degradin 2025-05-07 15:21:19 +03:00
parent 18afe9c214
commit 6f224ed1ef
3 changed files with 232 additions and 294 deletions

View File

@ -1,219 +1,150 @@
import React, { useEffect, useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useMedia } from '../contexts/MediaContext'; import { useNavigate } from 'react-router-dom';
import { listMedia } from '../services/supabase'; import { useAuth } from '../contexts/AuthContext';
import { mediaTypes } from '../services/mediaService'; import { supabase } from '../services/supabase';
import { useAuth } from "../contexts/AuthContext"; import MediaForm from '../components/admin/MediaForm';
import { useNavigate } from "react-router-dom";
import { createMedia } from "../services/supabase";
const AdminMediaPage = () => { const AdminMediaPage = () => {
const navigate = useNavigate();
const { user, userProfile, loading: authLoading } = useAuth();
const [media, setMedia] = useState([]); const [media, setMedia] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const navigate = useNavigate(); const [showForm, setShowForm] = useState(false);
const { currentUser, userProfile } = useAuth();
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [mediaData, setMediaData] = useState({
title: "",
type: "movie",
poster_url: "",
backdrop_url: "",
overview: "",
release_date: "",
is_published: false,
});
// Проверка прав доступа
if (!userProfile?.role || !["admin", "editor"].includes(userProfile.role)) {
return (
<div className="pt-20 container-custom py-12">
<div className="bg-status-error bg-opacity-20 text-status-error p-6 rounded-lg">
<h2 className="text-xl font-bold mb-2">Доступ запрещен</h2>
<p>У вас нет прав для доступа к этой странице.</p>
</div>
</div>
);
}
useEffect(() => { useEffect(() => {
const fetchMedia = async () => { console.log('AdminMediaPage mounted, user:', user);
try {
setLoading(true); if (!authLoading && !user) {
setError(null); console.log('No user, redirecting to login');
const data = await listMedia(null, 1, 100); // Получаем все медиа navigate('/login');
setMedia(data || []); return;
} catch (err) { }
console.error('Error fetching media:', err);
setError('Не удалось загрузить медиа');
} finally {
setLoading(false);
}
};
fetchMedia(); if (userProfile?.role !== 'admin') {
}, []); // Запускаем только при монтировании компонента console.log('Access denied');
navigate('/');
return;
}
const handleInputChange = (e) => { loadMedia();
const { name, value, type, checked } = e.target; }, [user, userProfile, authLoading, navigate]);
setMediaData((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value,
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError("");
const loadMedia = async () => {
try { try {
const newMedia = await createMedia({ setLoading(true);
...mediaData, const { data, error } = await supabase
created_by: currentUser.id, .from('media')
}); .select('*')
.order('created_at', { ascending: false });
setMedia(prev => [newMedia, ...prev]); if (error) throw error;
setMediaData({
title: "", setMedia(data || []);
type: "movie",
poster_url: "",
backdrop_url: "",
overview: "",
release_date: "",
is_published: false,
});
} catch (err) { } catch (err) {
setError("Ошибка при создании медиа. Пожалуйста, попробуйте снова."); console.error('Error loading media:', err);
console.error("Error creating media:", err); setError(err.message);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
if (loading) { const handleDelete = async (id) => {
return <div className="text-center">Загрузка...</div>; if (!window.confirm('Вы уверены, что хотите удалить этот медиа-контент?')) {
return;
}
try {
const { error } = await supabase
.from('media')
.delete()
.eq('id', id);
if (error) throw error;
setMedia(media.filter(item => item.id !== id));
} catch (err) {
console.error('Error deleting media:', err);
setError(err.message);
}
};
if (authLoading) {
return <div className="flex justify-center items-center h-screen">Загрузка...</div>;
} }
if (error) { if (!user || userProfile?.role !== 'admin') {
return <div className="text-red-500">{error}</div>; return null;
} }
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="container-custom pt-20">
<h1 className="text-3xl font-bold mb-8">Управление медиа</h1> <div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold text-campfire-amber">Управление медиа</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <button
{media.map((item) => ( onClick={() => setShowForm(true)}
<div key={`${item.id}-${item.type}`} className="bg-white rounded-lg shadow-md p-4"> className="btn-primary"
<h3 className="text-lg font-semibold mb-2">{item.title}</h3> >
<p className="text-gray-600 mb-2">Тип: {item.type}</p> Добавить медиа
{item.rating && ( </button>
<p className="text-gray-600">Рейтинг: {item.rating}</p>
)}
</div>
))}
</div> </div>
{/* Форма создания медиа */} {error && (
<div className="bg-campfire-charcoal p-6 rounded-lg mb-8"> <div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg mb-6">
<h2 className="text-2xl font-bold mb-4">Создать новое медиа</h2> {error}
{error && ( </div>
<div className="bg-status-error bg-opacity-20 text-status-error p-4 rounded-lg mb-4"> )}
{error}
</div> {loading ? (
)} <div className="text-center py-8">Загрузка...</div>
<form onSubmit={handleSubmit} className="space-y-4"> ) : media.length === 0 ? (
<div> <div className="text-center py-8 text-campfire-light">
<label className="block text-sm font-medium mb-1">Название</label> Медиа-контент не найден
<input </div>
type="text" ) : (
name="title" <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
value={mediaData.title} {media.map((item) => (
onChange={handleInputChange} <div
required key={`${item.id}-${item.type}`}
className="input w-full" className="bg-campfire-charcoal rounded-lg overflow-hidden border border-campfire-ash/20"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Тип</label>
<select
name="type"
value={mediaData.type}
onChange={handleInputChange}
className="input w-full"
> >
<option value="movie">Фильм</option> {item.poster_path && (
<option value="series">Сериал</option> <img
<option value="game">Игра</option> src={item.poster_path}
</select> alt={item.title}
</div> className="w-full h-48 object-cover"
/>
<div> )}
<label className="block text-sm font-medium mb-1">URL постера</label> <div className="p-4">
<input <h3 className="text-xl font-semibold text-campfire-amber mb-2">
type="url" {item.title}
name="poster_url" </h3>
value={mediaData.poster_url} <p className="text-campfire-light mb-4">
onChange={handleInputChange} {item.type === 'movie' ? 'Фильм' : 'Сериал'}
className="input w-full" </p>
/> <div className="flex justify-end space-x-2">
</div> <button
onClick={() => handleDelete(item.id)}
<div> className="text-red-500 hover:text-red-400"
<label className="block text-sm font-medium mb-1">URL фона</label> >
<input Удалить
type="url" </button>
name="backdrop_url" </div>
value={mediaData.backdrop_url} </div>
onChange={handleInputChange} </div>
className="input w-full" ))}
/> </div>
</div> )}
<div> {showForm && (
<label className="block text-sm font-medium mb-1">Описание</label> <MediaForm
<textarea onClose={() => setShowForm(false)}
name="overview" onSuccess={() => {
value={mediaData.overview} setShowForm(false);
onChange={handleInputChange} loadMedia();
className="input w-full h-32" }}
/> />
</div> )}
<div>
<label className="block text-sm font-medium mb-1">Дата выхода</label>
<input
type="date"
name="release_date"
value={mediaData.release_date}
onChange={handleInputChange}
className="input w-full"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
name="is_published"
checked={mediaData.is_published}
onChange={handleInputChange}
className="mr-2"
/>
<label className="text-sm font-medium">Опубликовать сразу</label>
</div>
<button
type="submit"
disabled={loading}
className="btn-primary w-full"
>
{loading ? "Создание..." : "Создать медиа"}
</button>
</form>
</div>
</div> </div>
); );
}; };

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from "react-router-dom"; import { useParams, Link } from 'react-router-dom';
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from '../contexts/AuthContext';
import { getUserProfile, getUserReviews } from "../services/supabase"; import { supabase } from '../services/supabase';
import { import {
FiEdit, FiEdit,
FiSettings, FiSettings,
@ -14,52 +14,59 @@ import {
import ReviewCard from "../components/reviews/ReviewCard"; import ReviewCard from "../components/reviews/ReviewCard";
import RatingChart from "../components/reviews/RatingChart"; import RatingChart from "../components/reviews/RatingChart";
function ProfilePage() { const ProfilePage = () => {
const { userId } = useParams();
const navigate = useNavigate();
const { currentUser, userProfile } = useAuth();
const [profile, setProfile] = useState(null); const [profile, setProfile] = useState(null);
const [reviews, setReviews] = useState([]); const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const { username } = useParams();
const { user } = useAuth();
useEffect(() => { useEffect(() => {
const loadProfile = async () => { const loadProfile = async () => {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
// Если userId не указан, показываем профиль текущего пользователя
const targetUserId = userId || currentUser?.id;
if (!targetUserId) {
navigate('/login');
return;
}
const profileData = await getUserProfile(targetUserId); // Получаем профиль по username
const { data: profileData, error: profileError } = await supabase
.from('users')
.select('*')
.eq('username', username)
.single();
if (profileError) throw profileError;
setProfile(profileData); setProfile(profileData);
const reviewsData = await getUserReviews(targetUserId); // Получаем отзывы пользователя
const { data: reviewsData, error: reviewsError } = await supabase
.from('reviews')
.select(`
*,
media(title, type, poster_url)
`)
.eq('user_id', profileData.id)
.order('created_at', { ascending: false });
if (reviewsError) throw reviewsError;
setReviews(reviewsData); setReviews(reviewsData);
} catch (err) { } catch (err) {
setError("Ошибка при загрузке профиля"); console.error('Error loading profile:', err);
console.error("Error loading profile:", err); setError('Не удалось загрузить профиль');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
loadProfile(); loadProfile();
}, [userId, currentUser, navigate]); }, [username]);
if (loading) { if (loading) {
return ( return (
<div className="pt-20 container-custom py-12"> <div className="min-h-screen bg-campfire-dark pt-20">
<div className="animate-pulse"> <div className="container-custom py-12">
<div className="h-32 bg-campfire-charcoal rounded-lg mb-6"></div> <div className="flex justify-center items-center h-64">
<div className="space-y-4"> <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-campfire-amber"></div>
<div className="h-4 bg-campfire-charcoal rounded w-1/4"></div>
<div className="h-4 bg-campfire-charcoal rounded w-1/2"></div>
</div> </div>
</div> </div>
</div> </div>
@ -68,10 +75,11 @@ function ProfilePage() {
if (error) { if (error) {
return ( return (
<div className="pt-20 container-custom py-12"> <div className="min-h-screen bg-campfire-dark pt-20">
<div className="bg-status-error bg-opacity-20 text-status-error p-6 rounded-lg"> <div className="container-custom py-12">
<h2 className="text-xl font-bold mb-2">Ошибка</h2> <div className="bg-status-error/20 text-status-error p-4 rounded-lg text-center">
<p>{error}</p> {error}
</div>
</div> </div>
</div> </div>
); );
@ -79,95 +87,94 @@ function ProfilePage() {
if (!profile) { if (!profile) {
return ( return (
<div className="pt-20 container-custom py-12"> <div className="min-h-screen bg-campfire-dark pt-20">
<div className="bg-status-error bg-opacity-20 text-status-error p-6 rounded-lg"> <div className="container-custom py-12">
<h2 className="text-xl font-bold mb-2">Профиль не найден</h2> <div className="text-center">
<p>Пользователь с таким ID не существует.</p> <h1 className="text-2xl font-bold text-campfire-light mb-4">
Профиль не найден
</h1>
<Link to="/" className="text-campfire-amber hover:text-campfire-ember">
Вернуться на главную
</Link>
</div>
</div> </div>
</div> </div>
); );
} }
const isOwnProfile = currentUser?.id === profile.id;
return ( return (
<div className="pt-20 container-custom py-12"> <div className="min-h-screen bg-campfire-dark pt-20">
<div className="bg-campfire-charcoal rounded-lg overflow-hidden"> <div className="container-custom py-12">
{/* Заголовок профиля */} <div className="bg-campfire-charcoal rounded-lg shadow-lg border border-campfire-ash/20 p-8">
<div className="relative h-48 bg-gradient-to-r from-campfire-amber to-campfire-ember"> <div className="flex items-center gap-6 mb-8">
{profile.profile_picture && ( <div className="w-24 h-24 rounded-full bg-campfire-ash/20 flex items-center justify-center">
<img {profile.profile_picture ? (
src={profile.profile_picture} <img
alt={profile.username} src={profile.profile_picture}
className="absolute -bottom-16 left-8 w-32 h-32 rounded-full border-4 border-campfire-charcoal object-cover" alt={profile.username}
/> className="w-full h-full rounded-full object-cover"
)} />
</div> ) : (
<span className="text-3xl text-campfire-amber">
<div className="pt-20 px-8 pb-8"> {profile.username[0].toUpperCase()}
<div className="flex justify-between items-start mb-6"> </span>
)}
</div>
<div> <div>
<h1 className="text-3xl font-bold mb-2">{profile.username}</h1> <h1 className="text-2xl font-bold text-campfire-light mb-2">
{profile.username}
</h1>
<p className="text-campfire-ash"> <p className="text-campfire-ash">
{profile.is_critic ? "Критик" : "Пользователь"} {profile.is_critic ? 'Критик' : 'Пользователь'}
</p> </p>
</div> </div>
{isOwnProfile && (
<button
onClick={() => navigate('/settings')}
className="btn-secondary"
>
Редактировать профиль
</button>
)}
</div> </div>
{profile.bio && ( <div className="space-y-8">
<div className="mb-8"> <div>
<h2 className="text-xl font-bold mb-2">О себе</h2> <h2 className="text-xl font-semibold text-campfire-light mb-4">
<p className="text-campfire-light">{profile.bio}</p> Отзывы
</div> </h2>
)} {reviews.length > 0 ? (
<div className="space-y-4">
{/* Отзывы пользователя */} {reviews.map((review) => (
<div> <div
<h2 className="text-2xl font-bold mb-4">Отзывы</h2> key={review.id}
{reviews.length === 0 ? ( className="bg-campfire-dark rounded-lg p-4 border border-campfire-ash/20"
<p className="text-campfire-ash">Пока нет отзывов</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{reviews.map((review) => (
<div key={review.id} className="card p-4">
<div className="flex items-center mb-4">
<img
src={review.media.poster_url}
alt={review.media.title}
className="w-16 h-24 object-cover rounded-lg mr-4"
/>
<div>
<h3 className="font-bold">{review.media.title}</h3>
<p className="text-sm text-campfire-ash">
{review.media.type === 'movie' ? 'Фильм' :
review.media.type === 'series' ? 'Сериал' : 'Игра'}
</p>
</div>
</div>
<p className="text-sm mb-4 line-clamp-3">{review.content}</p>
<button
onClick={() => navigate(`/media/${review.media_id}`)}
className="btn-secondary w-full"
> >
Читать полностью <div className="flex items-center gap-4 mb-2">
</button> <img
</div> src={review.media.poster_url}
))} alt={review.media.title}
</div> className="w-16 h-24 object-cover rounded"
)} />
<div>
<Link
to={`/media/${review.media_id}`}
className="text-lg font-medium text-campfire-light hover:text-campfire-amber"
>
{review.media.title}
</Link>
<p className="text-sm text-campfire-ash">
{new Date(review.created_at).toLocaleDateString()}
</p>
</div>
</div>
<p className="text-campfire-light">{review.content}</p>
</div>
))}
</div>
) : (
<p className="text-campfire-ash">
Пользователь еще не оставил ни одного отзыва
</p>
)}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} };
export default ProfilePage; export default ProfilePage;

View File

@ -10,7 +10,7 @@ function RegisterPage() {
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { signup } = useAuth(); const { signUp } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
@ -34,7 +34,7 @@ function RegisterPage() {
try { try {
setError(""); setError("");
setLoading(true); setLoading(true);
await signup(email, password, username); await signUp(email, password, username);
navigate("/"); navigate("/");
} catch (err) { } catch (err) {
setError( setError(