Исправления на страницах
This commit is contained in:
parent
18afe9c214
commit
6f224ed1ef
@ -1,219 +1,150 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useMedia } from '../contexts/MediaContext';
|
||||
import { listMedia } from '../services/supabase';
|
||||
import { mediaTypes } from '../services/mediaService';
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createMedia } from "../services/supabase";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { supabase } from '../services/supabase';
|
||||
import MediaForm from '../components/admin/MediaForm';
|
||||
|
||||
const AdminMediaPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, userProfile, loading: authLoading } = useAuth();
|
||||
const [media, setMedia] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
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>
|
||||
);
|
||||
}
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMedia = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await listMedia(null, 1, 100); // Получаем все медиа
|
||||
setMedia(data || []);
|
||||
} catch (err) {
|
||||
console.error('Error fetching media:', err);
|
||||
setError('Не удалось загрузить медиа');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
console.log('AdminMediaPage mounted, user:', user);
|
||||
|
||||
if (!authLoading && !user) {
|
||||
console.log('No user, redirecting to login');
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
fetchMedia();
|
||||
}, []); // Запускаем только при монтировании компонента
|
||||
if (userProfile?.role !== 'admin') {
|
||||
console.log('Access denied');
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
setMediaData((prev) => ({
|
||||
...prev,
|
||||
[name]: type === "checkbox" ? checked : value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
loadMedia();
|
||||
}, [user, userProfile, authLoading, navigate]);
|
||||
|
||||
const loadMedia = async () => {
|
||||
try {
|
||||
const newMedia = await createMedia({
|
||||
...mediaData,
|
||||
created_by: currentUser.id,
|
||||
});
|
||||
setLoading(true);
|
||||
const { data, error } = await supabase
|
||||
.from('media')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
setMedia(prev => [newMedia, ...prev]);
|
||||
setMediaData({
|
||||
title: "",
|
||||
type: "movie",
|
||||
poster_url: "",
|
||||
backdrop_url: "",
|
||||
overview: "",
|
||||
release_date: "",
|
||||
is_published: false,
|
||||
});
|
||||
if (error) throw error;
|
||||
|
||||
setMedia(data || []);
|
||||
} catch (err) {
|
||||
setError("Ошибка при создании медиа. Пожалуйста, попробуйте снова.");
|
||||
console.error("Error creating media:", err);
|
||||
console.error('Error loading media:', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center">Загрузка...</div>;
|
||||
const handleDelete = async (id) => {
|
||||
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) {
|
||||
return <div className="text-red-500">{error}</div>;
|
||||
if (!user || userProfile?.role !== 'admin') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-8">Управление медиа</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{media.map((item) => (
|
||||
<div key={`${item.id}-${item.type}`} className="bg-white rounded-lg shadow-md p-4">
|
||||
<h3 className="text-lg font-semibold mb-2">{item.title}</h3>
|
||||
<p className="text-gray-600 mb-2">Тип: {item.type}</p>
|
||||
{item.rating && (
|
||||
<p className="text-gray-600">Рейтинг: {item.rating}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="container-custom pt-20">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-campfire-amber">Управление медиа</h1>
|
||||
<button
|
||||
onClick={() => setShowForm(true)}
|
||||
className="btn-primary"
|
||||
>
|
||||
Добавить медиа
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Форма создания медиа */}
|
||||
<div className="bg-campfire-charcoal p-6 rounded-lg mb-8">
|
||||
<h2 className="text-2xl font-bold mb-4">Создать новое медиа</h2>
|
||||
{error && (
|
||||
<div className="bg-status-error bg-opacity-20 text-status-error p-4 rounded-lg mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Название</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
value={mediaData.title}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="input w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Тип</label>
|
||||
<select
|
||||
name="type"
|
||||
value={mediaData.type}
|
||||
onChange={handleInputChange}
|
||||
className="input w-full"
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg mb-6">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8">Загрузка...</div>
|
||||
) : media.length === 0 ? (
|
||||
<div className="text-center py-8 text-campfire-light">
|
||||
Медиа-контент не найден
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{media.map((item) => (
|
||||
<div
|
||||
key={`${item.id}-${item.type}`}
|
||||
className="bg-campfire-charcoal rounded-lg overflow-hidden border border-campfire-ash/20"
|
||||
>
|
||||
<option value="movie">Фильм</option>
|
||||
<option value="series">Сериал</option>
|
||||
<option value="game">Игра</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">URL постера</label>
|
||||
<input
|
||||
type="url"
|
||||
name="poster_url"
|
||||
value={mediaData.poster_url}
|
||||
onChange={handleInputChange}
|
||||
className="input w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">URL фона</label>
|
||||
<input
|
||||
type="url"
|
||||
name="backdrop_url"
|
||||
value={mediaData.backdrop_url}
|
||||
onChange={handleInputChange}
|
||||
className="input w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Описание</label>
|
||||
<textarea
|
||||
name="overview"
|
||||
value={mediaData.overview}
|
||||
onChange={handleInputChange}
|
||||
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>
|
||||
{item.poster_path && (
|
||||
<img
|
||||
src={item.poster_path}
|
||||
alt={item.title}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<h3 className="text-xl font-semibold text-campfire-amber mb-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-campfire-light mb-4">
|
||||
{item.type === 'movie' ? 'Фильм' : 'Сериал'}
|
||||
</p>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => handleDelete(item.id)}
|
||||
className="text-red-500 hover:text-red-400"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showForm && (
|
||||
<MediaForm
|
||||
onClose={() => setShowForm(false)}
|
||||
onSuccess={() => {
|
||||
setShowForm(false);
|
||||
loadMedia();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { getUserProfile, getUserReviews } from "../services/supabase";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { supabase } from '../services/supabase';
|
||||
import {
|
||||
FiEdit,
|
||||
FiSettings,
|
||||
@ -14,52 +14,59 @@ import {
|
||||
import ReviewCard from "../components/reviews/ReviewCard";
|
||||
import RatingChart from "../components/reviews/RatingChart";
|
||||
|
||||
function ProfilePage() {
|
||||
const { userId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { currentUser, userProfile } = useAuth();
|
||||
const ProfilePage = () => {
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [reviews, setReviews] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const { username } = useParams();
|
||||
const { user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
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);
|
||||
|
||||
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);
|
||||
} catch (err) {
|
||||
setError("Ошибка при загрузке профиля");
|
||||
console.error("Error loading profile:", err);
|
||||
console.error('Error loading profile:', err);
|
||||
setError('Не удалось загрузить профиль');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
}, [userId, currentUser, navigate]);
|
||||
}, [username]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pt-20 container-custom py-12">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-32 bg-campfire-charcoal rounded-lg mb-6"></div>
|
||||
<div className="space-y-4">
|
||||
<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 className="min-h-screen bg-campfire-dark pt-20">
|
||||
<div className="container-custom py-12">
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-campfire-amber"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,10 +75,11 @@ function ProfilePage() {
|
||||
|
||||
if (error) {
|
||||
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>{error}</p>
|
||||
<div className="min-h-screen bg-campfire-dark pt-20">
|
||||
<div className="container-custom py-12">
|
||||
<div className="bg-status-error/20 text-status-error p-4 rounded-lg text-center">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -79,95 +87,94 @@ function ProfilePage() {
|
||||
|
||||
if (!profile) {
|
||||
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>Пользователь с таким ID не существует.</p>
|
||||
<div className="min-h-screen bg-campfire-dark pt-20">
|
||||
<div className="container-custom py-12">
|
||||
<div className="text-center">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
const isOwnProfile = currentUser?.id === profile.id;
|
||||
|
||||
return (
|
||||
<div className="pt-20 container-custom py-12">
|
||||
<div className="bg-campfire-charcoal rounded-lg overflow-hidden">
|
||||
{/* Заголовок профиля */}
|
||||
<div className="relative h-48 bg-gradient-to-r from-campfire-amber to-campfire-ember">
|
||||
{profile.profile_picture && (
|
||||
<img
|
||||
src={profile.profile_picture}
|
||||
alt={profile.username}
|
||||
className="absolute -bottom-16 left-8 w-32 h-32 rounded-full border-4 border-campfire-charcoal object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-20 px-8 pb-8">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="min-h-screen bg-campfire-dark pt-20">
|
||||
<div className="container-custom py-12">
|
||||
<div className="bg-campfire-charcoal rounded-lg shadow-lg border border-campfire-ash/20 p-8">
|
||||
<div className="flex items-center gap-6 mb-8">
|
||||
<div className="w-24 h-24 rounded-full bg-campfire-ash/20 flex items-center justify-center">
|
||||
{profile.profile_picture ? (
|
||||
<img
|
||||
src={profile.profile_picture}
|
||||
alt={profile.username}
|
||||
className="w-full h-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-3xl text-campfire-amber">
|
||||
{profile.username[0].toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</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">
|
||||
{profile.is_critic ? "Критик" : "Пользователь"}
|
||||
{profile.is_critic ? 'Критик' : 'Пользователь'}
|
||||
</p>
|
||||
</div>
|
||||
{isOwnProfile && (
|
||||
<button
|
||||
onClick={() => navigate('/settings')}
|
||||
className="btn-secondary"
|
||||
>
|
||||
Редактировать профиль
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{profile.bio && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-bold mb-2">О себе</h2>
|
||||
<p className="text-campfire-light">{profile.bio}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Отзывы пользователя */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Отзывы</h2>
|
||||
{reviews.length === 0 ? (
|
||||
<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="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-campfire-light mb-4">
|
||||
Отзывы
|
||||
</h2>
|
||||
{reviews.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{reviews.map((review) => (
|
||||
<div
|
||||
key={review.id}
|
||||
className="bg-campfire-dark rounded-lg p-4 border border-campfire-ash/20"
|
||||
>
|
||||
Читать полностью
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<img
|
||||
src={review.media.poster_url}
|
||||
alt={review.media.title}
|
||||
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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
|
@ -10,7 +10,7 @@ function RegisterPage() {
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { signup } = useAuth();
|
||||
const { signUp } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
@ -34,7 +34,7 @@ function RegisterPage() {
|
||||
try {
|
||||
setError("");
|
||||
setLoading(true);
|
||||
await signup(email, password, username);
|
||||
await signUp(email, password, username);
|
||||
navigate("/");
|
||||
} catch (err) {
|
||||
setError(
|
||||
|
Loading…
Reference in New Issue
Block a user