Compare commits
7 Commits
690c18e601
...
d6bd38994a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d6bd38994a | ||
![]() |
6f224ed1ef | ||
![]() |
18afe9c214 | ||
![]() |
f5997a6c93 | ||
![]() |
0d958371e8 | ||
![]() |
977b0ec5e3 | ||
![]() |
5572b5f440 |
@ -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/:id" element={<ProfilePage />} />
|
<Route path="/profile/:username" 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/new" element={<AdminMediaPage />} />
|
<Route path="/admin/media" element={<AdminMediaPage />} />
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
191
src/components/admin/MediaForm.jsx
Normal file
191
src/components/admin/MediaForm.jsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
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;
|
@ -1,17 +1,17 @@
|
|||||||
import { useState, useEffect } from "react";
|
import React, { 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";
|
||||||
|
|
||||||
function Header() {
|
const 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 @@ function Header() {
|
|||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await logout();
|
await signOut();
|
||||||
navigate("/");
|
navigate("/");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Не удалось войти", error);
|
console.error("Не удалось выйти:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ function Header() {
|
|||||||
<FiSearch size={20} />
|
<FiSearch size={20} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{currentUser ? (
|
{user ? (
|
||||||
<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,8 +110,16 @@ function 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/${currentUser.uid}`}
|
to={`/profile/${userProfile?.username}`}
|
||||||
className="block px-4 py-2 hover:bg-campfire-dark"
|
className="block px-4 py-2 hover:bg-campfire-dark"
|
||||||
>
|
>
|
||||||
Профиль
|
Профиль
|
||||||
@ -125,9 +133,20 @@ function Header() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link to="/login" className="btn-primary">
|
<>
|
||||||
Войти
|
<Link
|
||||||
</Link>
|
to="/login"
|
||||||
|
className="text-campfire-light hover:text-campfire-amber transition-colors"
|
||||||
|
>
|
||||||
|
Войти
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="btn-primary"
|
||||||
|
>
|
||||||
|
Регистрация
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
@ -170,7 +189,7 @@ function Header() {
|
|||||||
Фильмы
|
Фильмы
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/discover/tv"
|
to="/discover/series"
|
||||||
className="text-campfire-light hover:text-campfire-amber transition-colors py-2"
|
className="text-campfire-light hover:text-campfire-amber transition-colors py-2"
|
||||||
>
|
>
|
||||||
Сериалы
|
Сериалы
|
||||||
@ -186,6 +205,6 @@ function Header() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
@ -29,116 +29,156 @@ export const AuthProvider = ({ children }) => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
// Функция для загрузки профиля пользователя
|
||||||
// Проверяем текущую сессию при загрузке
|
const loadUserProfile = async (userId) => {
|
||||||
const checkSession = async () => {
|
try {
|
||||||
try {
|
console.log('AuthProvider: Загрузка профиля пользователя:', userId);
|
||||||
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
|
const { data: profile, error: profileError } = await supabase
|
||||||
if (sessionError) throw sessionError;
|
.from('users')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
if (session?.user) {
|
if (profileError) {
|
||||||
setCurrentUser(session.user);
|
if (profileError.code === 'PGRST116') {
|
||||||
// Загружаем профиль пользователя
|
// Если профиль не найден, создаем новый
|
||||||
const { data: profile, error: profileError } = await supabase
|
console.log('AuthProvider: Профиль не найден, создаем новый');
|
||||||
|
const { data: newProfile, error: createError } = await supabase
|
||||||
.from('users')
|
.from('users')
|
||||||
.select('*')
|
.insert([
|
||||||
.eq('id', session.user.id)
|
{
|
||||||
|
id: userId,
|
||||||
|
username: `user_${userId.slice(0, 8)}`,
|
||||||
|
role: 'user'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (profileError) throw profileError;
|
if (createError) throw createError;
|
||||||
setUserProfile(profile);
|
console.log('AuthProvider: Новый профиль создан:', newProfile);
|
||||||
|
setUserProfile(newProfile);
|
||||||
|
} else {
|
||||||
|
throw profileError;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} else {
|
||||||
console.error('Error checking session:', err);
|
console.log('AuthProvider: Профиль загружен:', profile);
|
||||||
setError('Ошибка проверки сессии');
|
setUserProfile(profile);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error('AuthProvider: Ошибка загрузки профиля:', error);
|
||||||
|
setError(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
checkSession();
|
// Проверка сессии при загрузке
|
||||||
|
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;
|
||||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
|
||||||
if (event === 'SIGNED_IN' && session?.user) {
|
if (session?.user) {
|
||||||
|
console.log('AuthProvider: Пользователь найден в сессии');
|
||||||
setCurrentUser(session.user);
|
setCurrentUser(session.user);
|
||||||
try {
|
await loadUserProfile(session.user.id);
|
||||||
const { data: profile, error: profileError } = await supabase
|
} else {
|
||||||
.from('users')
|
console.log('AuthProvider: Пользователь не найден в сессии');
|
||||||
.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') {
|
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
setUserProfile(null);
|
setUserProfile(null);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AuthProvider: Ошибка проверки сессии:', error);
|
||||||
|
setError(error.message);
|
||||||
|
setCurrentUser(null);
|
||||||
|
setUserProfile(null);
|
||||||
|
} finally {
|
||||||
|
console.log('AuthProvider: Завершение проверки сессии');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('AuthProvider: Инициализация...');
|
||||||
|
checkSession();
|
||||||
|
|
||||||
|
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||||
|
console.log('AuthProvider: Изменение состояния авторизации:', { event, session });
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
if (event === 'SIGNED_IN') {
|
||||||
|
console.log('AuthProvider: Пользователь вошел в систему');
|
||||||
|
setCurrentUser(session.user);
|
||||||
|
await loadUserProfile(session.user.id);
|
||||||
|
} else if (event === 'SIGNED_OUT') {
|
||||||
|
console.log('AuthProvider: Пользователь вышел из системы');
|
||||||
|
setCurrentUser(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 (err) {
|
} catch (error) {
|
||||||
console.error('Error signing in:', err);
|
setError(error.message);
|
||||||
setError(err.message || 'Ошибка входа');
|
throw error;
|
||||||
throw err;
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const signUp = async (email, password, username) => {
|
const signUp = async (email, password) => {
|
||||||
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 (err) {
|
} catch (error) {
|
||||||
console.error('Error signing up:', err);
|
setError(error.message);
|
||||||
setError(err.message || 'Ошибка регистрации');
|
throw error;
|
||||||
throw err;
|
} finally {
|
||||||
|
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;
|
||||||
setCurrentUser(null);
|
} catch (error) {
|
||||||
setUserProfile(null);
|
setError(error.message);
|
||||||
} catch (err) {
|
throw error;
|
||||||
console.error('Error signing out:', err);
|
} finally {
|
||||||
setError('Ошибка при выходе');
|
setLoading(false);
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
currentUser,
|
user: currentUser,
|
||||||
userProfile,
|
userProfile,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
@ -147,13 +187,16 @@ export const AuthProvider = ({ children }) => {
|
|||||||
signOut
|
signOut
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
console.log('AuthProvider: Текущее состояние:', value);
|
||||||
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 (
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
<AuthContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -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);
|
|
||||||
setError(null);
|
|
||||||
const data = await listMedia(null, 1, 100); // Получаем все медиа
|
|
||||||
setMedia(data || []);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching media:', err);
|
|
||||||
setError('Не удалось загрузить медиа');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchMedia();
|
if (!authLoading && !user) {
|
||||||
}, []); // Запускаем только при монтировании компонента
|
console.log('No user, redirecting to login');
|
||||||
|
navigate('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleInputChange = (e) => {
|
if (userProfile?.role !== 'admin') {
|
||||||
const { name, value, type, checked } = e.target;
|
console.log('Access denied');
|
||||||
setMediaData((prev) => ({
|
navigate('/');
|
||||||
...prev,
|
return;
|
||||||
[name]: type === "checkbox" ? checked : value,
|
}
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
loadMedia();
|
||||||
e.preventDefault();
|
}, [user, userProfile, authLoading, navigate]);
|
||||||
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>
|
|
||||||
)}
|
|
||||||
<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>
|
{loading ? (
|
||||||
<label className="block text-sm font-medium mb-1">Тип</label>
|
<div className="text-center py-8">Загрузка...</div>
|
||||||
<select
|
) : media.length === 0 ? (
|
||||||
name="type"
|
<div className="text-center py-8 text-campfire-light">
|
||||||
value={mediaData.type}
|
Медиа-контент не найден
|
||||||
onChange={handleInputChange}
|
</div>
|
||||||
className="input w-full"
|
) : (
|
||||||
|
<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>
|
{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 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>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
{showForm && (
|
||||||
<label className="block text-sm font-medium mb-1">URL постера</label>
|
<MediaForm
|
||||||
<input
|
onClose={() => setShowForm(false)}
|
||||||
type="url"
|
onSuccess={() => {
|
||||||
name="poster_url"
|
setShowForm(false);
|
||||||
value={mediaData.poster_url}
|
loadMedia();
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,14 +14,13 @@ 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 () => {
|
||||||
@ -29,37 +28,45 @@ function ProfilePage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Если userId не указан, показываем профиль текущего пользователя
|
// Получаем профиль по username
|
||||||
const targetUserId = userId || currentUser?.id;
|
const { data: profileData, error: profileError } = await supabase
|
||||||
if (!targetUserId) {
|
.from('users')
|
||||||
navigate('/login');
|
.select('*')
|
||||||
return;
|
.eq('username', username)
|
||||||
}
|
.single();
|
||||||
|
|
||||||
const profileData = await getUserProfile(targetUserId);
|
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;
|
||||||
|
@ -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(
|
||||||
|
@ -12,36 +12,52 @@ 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: { username }
|
data: {
|
||||||
|
username: username
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) throw error;
|
if (authError) {
|
||||||
|
console.error('Ошибка при регистрации пользователя:', authError);
|
||||||
|
throw authError;
|
||||||
|
}
|
||||||
|
|
||||||
// Создаем профиль в таблице users
|
console.log('Пользователь создан в auth:', authData);
|
||||||
const { error: profileError } = await supabase
|
|
||||||
|
// Создание профиля пользователя
|
||||||
|
const { data: profileData, error: profileError } = await supabase
|
||||||
.from('users')
|
.from('users')
|
||||||
.insert({
|
.insert({
|
||||||
id: data.user.id,
|
id: authData.user.id,
|
||||||
email,
|
username: username,
|
||||||
username,
|
email: email,
|
||||||
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(data.user.id);
|
await supabase.auth.admin.deleteUser(authData.user.id);
|
||||||
throw profileError;
|
throw profileError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
console.log('Профиль пользователя создан:', profileData);
|
||||||
|
|
||||||
|
return { user: authData.user, profile: profileData };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Ошибка при регистрации:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -64,12 +80,12 @@ export const getCurrentUser = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// User functions
|
// User functions
|
||||||
export const getUserProfile = async (userId) => {
|
export const getUserProfile = async (username) => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('users')
|
.from('users')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('id', userId)
|
.eq('username', username)
|
||||||
.maybeSingle();
|
.single();
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return data;
|
return data;
|
||||||
|
65
supabase/migrations/20250705135500_update_schema.sql
Normal file
65
supabase/migrations/20250705135500_update_schema.sql
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
-- Включаем 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);
|
Loading…
Reference in New Issue
Block a user