import { useEffect, useMemo, useRef, useState } from 'react'; import { verifyAuth, fetchUsers, banUser, fetchPosts, updatePost, deletePost, removePostImage, banPostAuthor, fetchReports, updateReportStatus, publishToChannel } from './utils/api'; import { io } from 'socket.io-client'; import { Loader2, Users, Image as ImageIcon, ShieldCheck, SendHorizontal, MessageSquare, RefreshCw, Trash2, Edit, Ban } from 'lucide-react'; const TABS = [ { id: 'users', title: 'Пользователи', icon: Users }, { id: 'posts', title: 'Посты', icon: ImageIcon }, { id: 'reports', title: 'Репорты', icon: ShieldCheck }, { id: 'chat', title: 'Чат', icon: MessageSquare }, { id: 'publish', title: 'Публикация', icon: SendHorizontal } ]; const FILTERS = [ { id: 'active', label: 'Активные < 7д' }, { id: 'inactive', label: 'Неактивные' }, { id: 'banned', label: 'Бан' } ]; const slotOptions = Array.from({ length: 10 }, (_, i) => i + 1); const initialChatState = { messages: [], online: [], connected: false }; function formatDate(dateString) { if (!dateString) return '—'; const date = new Date(dateString); return date.toLocaleString('ru-RU', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }); } function classNames(...args) { return args.filter(Boolean).join(' '); } export default function App() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [tab, setTab] = useState('users'); // Users const [userFilter, setUserFilter] = useState('active'); const [usersData, setUsersData] = useState({ users: [], total: 0, totalPages: 1 }); const [usersLoading, setUsersLoading] = useState(false); // Posts const [postsData, setPostsData] = useState({ posts: [], totalPages: 1 }); const [postsLoading, setPostsLoading] = useState(false); // Reports const [reportsData, setReportsData] = useState({ reports: [], totalPages: 1 }); const [reportsLoading, setReportsLoading] = useState(false); // Publish const [publishState, setPublishState] = useState({ description: '', tags: '', slot: 1, files: [] }); const [publishing, setPublishing] = useState(false); // Chat const [chatState, setChatState] = useState(initialChatState); const [chatInput, setChatInput] = useState(''); const chatSocketRef = useRef(null); const chatListRef = useRef(null); const isTelegram = typeof window !== 'undefined' && window.Telegram?.WebApp; useEffect(() => { if (isTelegram) { window.Telegram.WebApp.disableVerticalSwipes(); window.Telegram.WebApp.ready(); window.Telegram.WebApp.expand(); } const init = async () => { try { const response = await verifyAuth(); setUser(response.data.user); if (isTelegram) { window.Telegram.WebApp.MainButton.setText('Закрыть'); window.Telegram.WebApp.MainButton.show(); window.Telegram.WebApp.MainButton.onClick(() => window.Telegram.WebApp.close()); } setLoading(false); } catch (err) { console.error(err); setError('Нет доступа. Убедитесь, что вы добавлены как администратор.'); setLoading(false); } }; init(); return () => { if (isTelegram) { window.Telegram.WebApp.MainButton.hide(); } }; }, [isTelegram]); useEffect(() => { if (tab === 'users') { loadUsers(); } else if (tab === 'posts') { loadPosts(); } else if (tab === 'reports') { loadReports(); } else if (tab === 'chat' && user) { initChat(); } return () => { if (tab !== 'chat' && chatSocketRef.current) { chatSocketRef.current.disconnect(); chatSocketRef.current = null; setChatState(initialChatState); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [tab, user, userFilter]); const loadUsers = async () => { setUsersLoading(true); try { const data = await fetchUsers({ filter: userFilter }); setUsersData(data); } catch (err) { console.error(err); } finally { setUsersLoading(false); } }; const loadPosts = async () => { setPostsLoading(true); try { const data = await fetchPosts(); setPostsData(data); } catch (err) { console.error(err); } finally { setPostsLoading(false); } }; const loadReports = async () => { setReportsLoading(true); try { const data = await fetchReports(); setReportsData(data); } catch (err) { console.error(err); } finally { setReportsLoading(false); } }; const initChat = () => { if (!user || chatSocketRef.current) return; const socket = io('/mod-chat', { transports: ['websocket', 'polling'] }); socket.on('connect', () => { socket.emit('auth', { username: user.username, telegramId: user.telegramId }); }); socket.on('ready', () => { setChatState((prev) => ({ ...prev, connected: true })); }); socket.on('unauthorized', () => { setChatState((prev) => ({ ...prev, connected: false })); socket.disconnect(); }); socket.on('message', (message) => { setChatState((prev) => ({ ...prev, messages: [...prev.messages, message] })); if (chatListRef.current) { chatListRef.current.scrollTo({ top: chatListRef.current.scrollHeight, behavior: 'smooth' }); } }); socket.on('online', (online) => { setChatState((prev) => ({ ...prev, online })); }); socket.on('disconnect', () => { setChatState((prev) => ({ ...prev, connected: false })); }); chatSocketRef.current = socket; }; const handleSendChat = () => { if (!chatSocketRef.current || !chatState.connected) return; const text = chatInput.trim(); if (!text) return; chatSocketRef.current.emit('message', { text }); setChatInput(''); }; const handleBanUser = async (id, banned) => { const days = banned ? parseInt(prompt('Введите срок бана в днях', '7'), 10) : 0; await banUser(id, { banned, days }); loadUsers(); }; const handlePostEdit = async (post) => { const newContent = prompt('Новый текст поста', post.content || ''); if (newContent === null) return; await updatePost(post.id, { content: newContent }); loadPosts(); }; const handlePostDelete = async (postId) => { if (!window.confirm('Удалить пост?')) return; await deletePost(postId); loadPosts(); }; const handleRemoveImage = async (postId, index) => { await removePostImage(postId, index); loadPosts(); }; const handleBanAuthor = async (postId) => { const days = parseInt(prompt('Срок бана автора (в днях)', '7'), 10); await banPostAuthor(postId, { days }); loadPosts(); loadUsers(); }; const handleReportStatus = async (reportId, status) => { await updateReportStatus(reportId, { status }); loadReports(); }; const handlePublish = async () => { if (!publishState.files.length) { alert('Добавьте изображения'); return; } setPublishing(true); try { const formData = new FormData(); publishState.files.forEach((file) => formData.append('images', file)); formData.append('description', publishState.description); formData.append('tags', JSON.stringify( publishState.tags .split(/[,\s]+/) .map((tag) => tag.trim()) .filter(Boolean) )); formData.append('slot', publishState.slot); await publishToChannel(formData); setPublishState({ description: '', tags: '', slot: 1, files: [] }); alert('Опубликовано в канал @reichenbfurry'); } catch (err) { console.error(err); alert('Не удалось опубликовать пост'); } finally { setPublishing(false); } }; const handleFileChange = (event) => { const files = Array.from(event.target.files || []).slice(0, 10); setPublishState((prev) => ({ ...prev, files })); }; const renderUsers = () => (

Пользователи

{FILTERS.map((filter) => ( ))}
{usersLoading ? (
) : (
{usersData.users.map((u) => (
@{u.username}
{u.firstName} {u.lastName || ''}
Роль: {u.role} Активность: {formatDate(u.lastActiveAt)} {u.banned && Бан до {formatDate(u.bannedUntil)}}
{u.banned ? ( ) : ( )}
))}
)}
); const renderPosts = () => (

Посты

{postsLoading ? (
) : (
{postsData.posts.map((post) => (
Автор: @{post.author?.username || 'Удалён'} — {formatDate(post.createdAt)}
{post.content || 'Без текста'}
Лайки: {post.likesCount} Комментарии: {post.commentsCount} {post.isNSFW && NSFW}
{post.images?.length ? (
{post.images.map((img, idx) => (
))}
) : null}
))}
)}
); const renderReports = () => (

Репорты

{reportsLoading ? (
) : (
{reportsData.reports.map((report) => (
Репорт от @{report.reporter?.username || 'Unknown'} — {formatDate(report.createdAt)}
Статус: {report.status}

{report.reason || 'Причина не указана'}

{report.post && (
Пост: {report.post.content || 'Без текста'}
)}
))}
)}
); const renderChat = () => (

Лайвчат админов

{chatState.connected ? ( В сети ) : ( Подключение... )}
Онлайн:{' '} {chatState.online.length ? chatState.online.map((admin) => `@${admin.username}`).join(', ') : '—'}
{chatState.messages.map((message) => (
@{message.username}
{message.text}
{formatDate(message.createdAt)}
))}
setChatInput(e.target.value)} placeholder="Сообщение для админов..." />
); const renderPublish = () => (

Публикация в @reichenbfurry