Compare commits

..

No commits in common. "d6bd38994a7ebb31a6b0a2e84e9a7ed8ff547744" and "690c18e6011f58ca853a03e230f287904c6e1de8" have entirely different histories.

9 changed files with 399 additions and 671 deletions

View File

@ -22,10 +22,10 @@ function App() {
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/media/:id" element={<MediaPage />} /> <Route path="/media/:id" element={<MediaPage />} />
<Route path="/profile/:username" element={<ProfilePage />} /> <Route path="/profile/:id" element={<ProfilePage />} />
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} /> <Route path="/register" element={<RegisterPage />} />
<Route path="/admin/media" element={<AdminMediaPage />} /> <Route path="/admin/media/new" element={<AdminMediaPage />} />
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
</main> </main>

View File

@ -1,191 +0,0 @@
import React, { useState } from 'react';
import { supabase } from '../../services/supabase';
const MediaForm = ({ onSuccess, onCancel }) => {
const [formData, setFormData] = useState({
title: '',
type: 'movie',
overview: '',
release_date: '',
poster_url: '',
backdrop_url: '',
is_published: true
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const { data, error } = await supabase
.from('media')
.insert([{
...formData,
created_by: (await supabase.auth.getUser()).data.user.id
}])
.select()
.single();
if (error) throw error;
onSuccess(data);
} catch (error) {
console.error('Error creating media:', error);
setError(error.message);
} finally {
setLoading(false);
}
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
return (
<div className="fixed inset-0 bg-campfire-charcoal/80 flex items-center justify-center p-4 z-50">
<div className="bg-campfire-dark rounded-lg shadow-xl max-w-2xl w-full p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-campfire-light">
Добавить новый контент
</h2>
<button
onClick={onCancel}
className="text-campfire-ash hover:text-campfire-light"
>
</button>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg mb-6">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-campfire-light mb-1">
Название
</label>
<input
type="text"
name="title"
value={formData.title}
onChange={handleChange}
className="input w-full"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-campfire-light mb-1">
Тип
</label>
<select
name="type"
value={formData.type}
onChange={handleChange}
className="input w-full"
required
>
<option value="movie">Фильм</option>
<option value="tv">Сериал</option>
<option value="game">Игра</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-campfire-light mb-1">
Описание
</label>
<textarea
name="overview"
value={formData.overview}
onChange={handleChange}
className="input w-full h-32"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-campfire-light mb-1">
Дата выхода
</label>
<input
type="date"
name="release_date"
value={formData.release_date}
onChange={handleChange}
className="input w-full"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-campfire-light mb-1">
URL постера
</label>
<input
type="url"
name="poster_url"
value={formData.poster_url}
onChange={handleChange}
className="input w-full"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-campfire-light mb-1">
URL фона
</label>
<input
type="url"
name="backdrop_url"
value={formData.backdrop_url}
onChange={handleChange}
className="input w-full"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
name="is_published"
checked={formData.is_published}
onChange={handleChange}
className="mr-2"
/>
<label className="text-sm font-medium">Опубликовать сразу</label>
</div>
<div className="flex justify-end gap-4 mt-6">
<button
type="button"
onClick={onCancel}
className="btn-secondary"
>
Отмена
</button>
<button
type="submit"
className="btn-primary"
disabled={loading}
>
{loading ? 'Создание...' : 'Создать'}
</button>
</div>
</form>
</div>
</div>
);
};
export default MediaForm;

View File

@ -1,17 +1,17 @@
import React, { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { Link, useNavigate, useLocation } from "react-router-dom"; import { Link, useNavigate, useLocation } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext"; import { useAuth } from "../../contexts/AuthContext";
import { FiSearch, FiMenu, FiX, FiUser } from "react-icons/fi"; import { FiSearch, FiMenu, FiX, FiUser } from "react-icons/fi";
import SearchBar from "../ui/SearchBar"; import SearchBar from "../ui/SearchBar";
import Logo from "../ui/Logo"; import Logo from "../ui/Logo";
const Header = () => { function Header() {
const { user, userProfile, signOut } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false);
const { currentUser, userProfile, logout } = useAuth();
const navigate = useNavigate();
const location = useLocation();
// Close mobile menu when route changes // Close mobile menu when route changes
useEffect(() => { useEffect(() => {
@ -35,10 +35,10 @@ const Header = () => {
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await signOut(); await logout();
navigate("/"); navigate("/");
} catch (error) { } catch (error) {
console.error("Не удалось выйти:", error); console.error("Не удалось войти", error);
} }
}; };
@ -96,7 +96,7 @@ const Header = () => {
<FiSearch size={20} /> <FiSearch size={20} />
</button> </button>
{user ? ( {currentUser ? (
<div className="relative group"> <div className="relative group">
<button className="flex items-center space-x-2 p-2 rounded-full bg-campfire-charcoal"> <button className="flex items-center space-x-2 p-2 rounded-full bg-campfire-charcoal">
{userProfile?.profilePicture ? ( {userProfile?.profilePicture ? (
@ -110,16 +110,8 @@ const Header = () => {
)} )}
</button> </button>
<div className="absolute right-0 mt-2 w-48 py-2 bg-campfire-charcoal rounded-md shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300"> <div className="absolute right-0 mt-2 w-48 py-2 bg-campfire-charcoal rounded-md shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300">
{userProfile?.role === 'admin' && (
<Link
to="/admin/media"
className="block px-4 py-2 hover:bg-campfire-dark"
>
Админ панель
</Link>
)}
<Link <Link
to={`/profile/${userProfile?.username}`} to={`/profile/${currentUser.uid}`}
className="block px-4 py-2 hover:bg-campfire-dark" className="block px-4 py-2 hover:bg-campfire-dark"
> >
Профиль Профиль
@ -133,20 +125,9 @@ const Header = () => {
</div> </div>
</div> </div>
) : ( ) : (
<> <Link to="/login" className="btn-primary">
<Link Войти
to="/login" </Link>
className="text-campfire-light hover:text-campfire-amber transition-colors"
>
Войти
</Link>
<Link
to="/register"
className="btn-primary"
>
Регистрация
</Link>
</>
)} )}
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
@ -189,7 +170,7 @@ const Header = () => {
Фильмы Фильмы
</Link> </Link>
<Link <Link
to="/discover/series" to="/discover/tv"
className="text-campfire-light hover:text-campfire-amber transition-colors py-2" className="text-campfire-light hover:text-campfire-amber transition-colors py-2"
> >
Сериалы Сериалы
@ -205,6 +186,6 @@ const Header = () => {
</div> </div>
</header> </header>
); );
}; }
export default Header; export default Header;

View File

@ -29,156 +29,116 @@ export const AuthProvider = ({ children }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
// Функция для загрузки профиля пользователя
const loadUserProfile = async (userId) => {
try {
console.log('AuthProvider: Загрузка профиля пользователя:', userId);
const { data: profile, error: profileError } = await supabase
.from('users')
.select('*')
.eq('id', userId)
.single();
if (profileError) {
if (profileError.code === 'PGRST116') {
// Если профиль не найден, создаем новый
console.log('AuthProvider: Профиль не найден, создаем новый');
const { data: newProfile, error: createError } = await supabase
.from('users')
.insert([
{
id: userId,
username: `user_${userId.slice(0, 8)}`,
role: 'user'
}
])
.select()
.single();
if (createError) throw createError;
console.log('AuthProvider: Новый профиль создан:', newProfile);
setUserProfile(newProfile);
} else {
throw profileError;
}
} else {
console.log('AuthProvider: Профиль загружен:', profile);
setUserProfile(profile);
}
} catch (error) {
console.error('AuthProvider: Ошибка загрузки профиля:', error);
setError(error.message);
}
};
// Проверка сессии при загрузке
const checkSession = async () => {
try {
console.log('AuthProvider: Проверка сессии...');
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
console.log('AuthProvider: Результат проверки сессии:', { session, error: sessionError });
if (sessionError) throw sessionError;
if (session?.user) {
console.log('AuthProvider: Пользователь найден в сессии');
setCurrentUser(session.user);
await loadUserProfile(session.user.id);
} else {
console.log('AuthProvider: Пользователь не найден в сессии');
setCurrentUser(null);
setUserProfile(null);
}
} catch (error) {
console.error('AuthProvider: Ошибка проверки сессии:', error);
setError(error.message);
setCurrentUser(null);
setUserProfile(null);
} finally {
console.log('AuthProvider: Завершение проверки сессии');
setLoading(false);
}
};
useEffect(() => { useEffect(() => {
console.log('AuthProvider: Инициализация...'); // Проверяем текущую сессию при загрузке
const checkSession = async () => {
try {
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
if (sessionError) throw sessionError;
if (session?.user) {
setCurrentUser(session.user);
// Загружаем профиль пользователя
const { data: profile, error: profileError } = await supabase
.from('users')
.select('*')
.eq('id', session.user.id)
.single();
if (profileError) throw profileError;
setUserProfile(profile);
}
} catch (err) {
console.error('Error checking session:', err);
setError('Ошибка проверки сессии');
} finally {
setLoading(false);
}
};
checkSession(); checkSession();
// Подписываемся на изменения состояния авторизации
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => { const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
console.log('AuthProvider: Изменение состояния авторизации:', { event, session }); if (event === 'SIGNED_IN' && session?.user) {
setLoading(true);
if (event === 'SIGNED_IN') {
console.log('AuthProvider: Пользователь вошел в систему');
setCurrentUser(session.user); setCurrentUser(session.user);
await loadUserProfile(session.user.id); try {
const { data: profile, error: profileError } = await supabase
.from('users')
.select('*')
.eq('id', session.user.id)
.single();
if (profileError) throw profileError;
setUserProfile(profile);
} catch (err) {
console.error('Error loading user profile:', err);
setError('Ошибка загрузки профиля');
}
} else if (event === 'SIGNED_OUT') { } else if (event === 'SIGNED_OUT') {
console.log('AuthProvider: Пользователь вышел из системы');
setCurrentUser(null); setCurrentUser(null);
setUserProfile(null); setUserProfile(null);
} }
setLoading(false);
}); });
return () => { return () => {
console.log('AuthProvider: Отписка от изменений состояния авторизации');
subscription.unsubscribe(); subscription.unsubscribe();
}; };
}, []); }, []);
const signIn = async (email, password) => { const signIn = async (email, password) => {
try { try {
setLoading(true);
setError(null); setError(null);
const { data, error } = await supabase.auth.signInWithPassword({ const { data, error } = await supabase.auth.signInWithPassword({
email, email,
password password
}); });
if (error) throw error; if (error) throw error;
return data; return data;
} catch (error) { } catch (err) {
setError(error.message); console.error('Error signing in:', err);
throw error; setError(err.message || 'Ошибка входа');
} finally { throw err;
setLoading(false);
} }
}; };
const signUp = async (email, password) => { const signUp = async (email, password, username) => {
try { try {
setLoading(true);
setError(null); setError(null);
const { data, error } = await supabase.auth.signUp({ const { data, error } = await supabase.auth.signUp({
email, email,
password password,
options: {
data: { username }
}
}); });
if (error) throw error; if (error) throw error;
return data; return data;
} catch (error) { } catch (err) {
setError(error.message); console.error('Error signing up:', err);
throw error; setError(err.message || 'Ошибка регистрации');
} finally { throw err;
setLoading(false);
} }
}; };
const signOut = async () => { const signOut = async () => {
try { try {
setLoading(true);
setError(null); setError(null);
const { error } = await supabase.auth.signOut(); const { error } = await supabase.auth.signOut();
if (error) throw error; if (error) throw error;
} catch (error) { setCurrentUser(null);
setError(error.message); setUserProfile(null);
throw error; } catch (err) {
} finally { console.error('Error signing out:', err);
setLoading(false); setError('Ошибка при выходе');
throw err;
} }
}; };
const value = { const value = {
user: currentUser, currentUser,
userProfile, userProfile,
loading, loading,
error, error,
@ -187,16 +147,13 @@ export const AuthProvider = ({ children }) => {
signOut signOut
}; };
console.log('AuthProvider: Текущее состояние:', value); if (loading) {
return <div className="flex justify-center items-center h-screen">Загрузка...</div>;
if (loading || (currentUser && !userProfile)) {
console.log('AuthProvider: Отображение состояния загрузки');
return (
<div className="min-h-screen flex items-center justify-center bg-campfire-dark">
<div className="text-campfire-amber">Загрузка...</div>
</div>
);
} }
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}; };

View File

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

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { useParams, Link } from 'react-router-dom'; import { useParams, useNavigate } from "react-router-dom";
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from "../contexts/AuthContext";
import { supabase } from '../services/supabase'; import { getUserProfile, getUserReviews } from "../services/supabase";
import { import {
FiEdit, FiEdit,
FiSettings, FiSettings,
@ -14,59 +14,52 @@ 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";
const ProfilePage = () => { function 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;
}
// Получаем профиль по username const profileData = await getUserProfile(targetUserId);
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) {
console.error('Error loading profile:', err); setError("Ошибка при загрузке профиля");
setError('Не удалось загрузить профиль'); console.error("Error loading profile:", err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
loadProfile(); loadProfile();
}, [username]); }, [userId, currentUser, navigate]);
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-campfire-dark pt-20"> <div className="pt-20 container-custom py-12">
<div className="container-custom py-12"> <div className="animate-pulse">
<div className="flex justify-center items-center h-64"> <div className="h-32 bg-campfire-charcoal rounded-lg mb-6"></div>
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-campfire-amber"></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> </div>
</div> </div>
</div> </div>
@ -75,11 +68,10 @@ const ProfilePage = () => {
if (error) { if (error) {
return ( return (
<div className="min-h-screen bg-campfire-dark pt-20"> <div className="pt-20 container-custom py-12">
<div className="container-custom py-12"> <div className="bg-status-error bg-opacity-20 text-status-error p-6 rounded-lg">
<div className="bg-status-error/20 text-status-error p-4 rounded-lg text-center"> <h2 className="text-xl font-bold mb-2">Ошибка</h2>
{error} <p>{error}</p>
</div>
</div> </div>
</div> </div>
); );
@ -87,94 +79,95 @@ const ProfilePage = () => {
if (!profile) { if (!profile) {
return ( return (
<div className="min-h-screen bg-campfire-dark pt-20"> <div className="pt-20 container-custom py-12">
<div className="container-custom py-12"> <div className="bg-status-error bg-opacity-20 text-status-error p-6 rounded-lg">
<div className="text-center"> <h2 className="text-xl font-bold mb-2">Профиль не найден</h2>
<h1 className="text-2xl font-bold text-campfire-light mb-4"> <p>Пользователь с таким ID не существует.</p>
Профиль не найден
</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="min-h-screen bg-campfire-dark pt-20"> <div className="pt-20 container-custom py-12">
<div className="container-custom py-12"> <div className="bg-campfire-charcoal rounded-lg overflow-hidden">
<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="relative h-48 bg-gradient-to-r from-campfire-amber to-campfire-ember">
<div className="w-24 h-24 rounded-full bg-campfire-ash/20 flex items-center justify-center"> {profile.profile_picture && (
{profile.profile_picture ? ( <img
<img src={profile.profile_picture}
src={profile.profile_picture} alt={profile.username}
alt={profile.username} className="absolute -bottom-16 left-8 w-32 h-32 rounded-full border-4 border-campfire-charcoal object-cover"
className="w-full h-full rounded-full object-cover" />
/> )}
) : ( </div>
<span className="text-3xl text-campfire-amber">
{profile.username[0].toUpperCase()} <div className="pt-20 px-8 pb-8">
</span> <div className="flex justify-between items-start mb-6">
)}
</div>
<div> <div>
<h1 className="text-2xl font-bold text-campfire-light mb-2"> <h1 className="text-3xl font-bold mb-2">{profile.username}</h1>
{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>
<div className="space-y-8"> {profile.bio && (
<div> <div className="mb-8">
<h2 className="text-xl font-semibold text-campfire-light mb-4"> <h2 className="text-xl font-bold mb-2">О себе</h2>
Отзывы <p className="text-campfire-light">{profile.bio}</p>
</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"
>
<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>
<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"
>
Читать полностью
</button>
</div>
))}
</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(

View File

@ -12,52 +12,36 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey);
// Auth functions // Auth functions
export const signUp = async (email, password, username) => { export const signUp = async (email, password, username) => {
try { try {
console.log('Начало регистрации:', { email, username }); // Регистрируем пользователя
const { data, error } = await supabase.auth.signUp({
// Регистрация пользователя
const { data: authData, error: authError } = await supabase.auth.signUp({
email, email,
password, password,
options: { options: {
data: { data: { username }
username: username
}
} }
}); });
if (authError) { if (error) throw error;
console.error('Ошибка при регистрации пользователя:', authError);
throw authError; // Создаем профиль в таблице users
} const { error: profileError } = await supabase
console.log('Пользователь создан в auth:', authData);
// Создание профиля пользователя
const { data: profileData, error: profileError } = await supabase
.from('users') .from('users')
.insert({ .insert({
id: authData.user.id, id: data.user.id,
username: username, email,
email: email, username,
role: 'user', role: 'user',
is_critic: false,
created_at: new Date().toISOString() created_at: new Date().toISOString()
}) });
.select()
.single();
if (profileError) { if (profileError) {
console.error('Ошибка при создании профиля:', profileError);
// Если не удалось создать профиль, удаляем пользователя // Если не удалось создать профиль, удаляем пользователя
await supabase.auth.admin.deleteUser(authData.user.id); await supabase.auth.admin.deleteUser(data.user.id);
throw profileError; throw profileError;
} }
console.log('Профиль пользователя создан:', profileData); return data;
return { user: authData.user, profile: profileData };
} catch (error) { } catch (error) {
console.error('Ошибка при регистрации:', error);
throw error; throw error;
} }
}; };
@ -80,12 +64,12 @@ export const getCurrentUser = async () => {
}; };
// User functions // User functions
export const getUserProfile = async (username) => { export const getUserProfile = async (userId) => {
const { data, error } = await supabase const { data, error } = await supabase
.from('users') .from('users')
.select('*') .select('*')
.eq('username', username) .eq('id', userId)
.single(); .maybeSingle();
if (error) throw error; if (error) throw error;
return data; return data;

View File

@ -1,65 +0,0 @@
-- Включаем RLS для всех таблиц
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE media ENABLE ROW LEVEL SECURITY;
ALTER TABLE reviews ENABLE ROW LEVEL SECURITY;
-- Политики для таблицы users
CREATE POLICY "Users can view own profile"
ON users FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON users FOR UPDATE
USING (auth.uid() = id);
-- Политики для таблицы media
CREATE POLICY "Anyone can view media"
ON media FOR SELECT
USING (true);
CREATE POLICY "Admins can insert media"
ON media FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role = 'admin'
)
);
CREATE POLICY "Admins can update media"
ON media FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role = 'admin'
)
);
CREATE POLICY "Admins can delete media"
ON media FOR DELETE
USING (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role = 'admin'
)
);
-- Политики для таблицы reviews
CREATE POLICY "Anyone can view reviews"
ON reviews FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can insert reviews"
ON reviews FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "Users can update own reviews"
ON reviews FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own reviews"
ON reviews FOR DELETE
USING (auth.uid() = user_id);