Update files

This commit is contained in:
glpshchn 2025-11-04 01:51:17 +03:00
parent d90f6088d2
commit a678b129c8
11 changed files with 1024 additions and 36 deletions

View File

@ -2,14 +2,33 @@
const axios = require('axios');
const config = require('./config');
const TELEGRAM_API = `https://api.telegram.org/bot${config.telegramBotToken}`;
if (!config.telegramBotToken) {
console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен! Функция отправки фото в Telegram недоступна.');
}
const TELEGRAM_API = config.telegramBotToken
? `https://api.telegram.org/bot${config.telegramBotToken}`
: null;
// Отправить одно фото пользователю
async function sendPhotoToUser(userId, photoUrl, caption) {
if (!TELEGRAM_API) {
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
}
try {
// Если photoUrl относительный (начинается с /), преобразуем в полный URL
let finalPhotoUrl = photoUrl;
if (photoUrl.startsWith('/')) {
// Если это прокси URL, нужно получить полный URL
// Для production используем домен из переменной окружения или дефолтный
const baseUrl = process.env.FRONTEND_URL || process.env.API_URL || 'https://nakama.glpshchn.ru';
finalPhotoUrl = `${baseUrl}${photoUrl}`;
}
const response = await axios.post(`${TELEGRAM_API}/sendPhoto`, {
chat_id: userId,
photo: photoUrl,
photo: finalPhotoUrl,
caption: caption || '',
parse_mode: 'HTML'
});
@ -23,6 +42,10 @@ async function sendPhotoToUser(userId, photoUrl, caption) {
// Отправить несколько фото группой (до 10 штук)
async function sendPhotosToUser(userId, photos) {
if (!TELEGRAM_API) {
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
}
try {
// Telegram поддерживает до 10 фото в одной группе
const batches = [];
@ -31,14 +54,23 @@ async function sendPhotosToUser(userId, photos) {
}
const results = [];
const baseUrl = process.env.FRONTEND_URL || process.env.API_URL || 'https://nakama.glpshchn.ru';
for (const batch of batches) {
const media = batch.map((photo, index) => ({
type: 'photo',
media: photo.url,
caption: index === 0 ? `<b>Из NakamaSpace</b>\n${batch.length} фото` : undefined,
parse_mode: 'HTML'
}));
const media = batch.map((photo, index) => {
// Преобразуем относительные URL в полные
let photoUrl = photo.url;
if (photoUrl.startsWith('/')) {
photoUrl = `${baseUrl}${photoUrl}`;
}
return {
type: 'photo',
media: photoUrl,
caption: index === 0 ? `<b>Из NakamaSpace</b>\n${batch.length} фото` : undefined,
parse_mode: 'HTML'
};
});
const response = await axios.post(`${TELEGRAM_API}/sendMediaGroup`, {
chat_id: userId,

View File

@ -17,6 +17,10 @@ module.exports = {
// Telegram
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN,
// Gelbooru API
gelbooruApiKey: process.env.GELBOORU_API_KEY || '638e2433d451fc02e848811acdafdce08317073c01ed78e38139115c19fe04afa367f736726514ef1337565d4c05b3cbe2c81125c424301e90d29d1f7f4cceff',
gelbooruUserId: process.env.GELBOORU_USER_ID || '1844464',
// Frontend URL
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:5173',

View File

@ -2,6 +2,7 @@ const express = require('express');
const router = express.Router();
const axios = require('axios');
const { authenticate } = require('../middleware/auth');
const config = require('../config');
// Функция для создания прокси URL
function createProxyUrl(originalUrl) {
@ -117,7 +118,9 @@ router.get('/anime', authenticate, async (req, res) => {
json: 1,
tags: query,
limit,
pid: page
pid: page,
api_key: config.gelbooruApiKey,
user_id: config.gelbooruUserId
},
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
@ -204,7 +207,9 @@ router.get('/anime/tags', authenticate, async (req, res) => {
json: 1,
name_pattern: `${query}%`,
orderby: 'count',
limit: 10
limit: 10,
api_key: config.gelbooruApiKey,
user_id: config.gelbooruUserId
},
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'

View File

@ -9,6 +9,8 @@ import Search from './pages/Search'
import Notifications from './pages/Notifications'
import Profile from './pages/Profile'
import UserProfile from './pages/UserProfile'
import CommentsPage from './pages/CommentsPage'
import PostMenuPage from './pages/PostMenuPage'
import './styles/index.css'
function App() {
@ -92,6 +94,8 @@ function App() {
<Route path="notifications" element={<Notifications user={user} />} />
<Route path="profile" element={<Profile user={user} setUser={setUser} />} />
<Route path="user/:id" element={<UserProfile currentUser={user} />} />
<Route path="post/:postId/comments" element={<CommentsPage user={user} />} />
<Route path="post/:postId/menu" element={<PostMenuPage user={user} />} />
</Route>
</Routes>
</BrowserRouter>

View File

@ -1,10 +1,8 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight } from 'lucide-react'
import { likePost, commentPost, deletePost } from '../utils/api'
import { likePost, deletePost } from '../utils/api'
import { hapticFeedback, showConfirm } from '../utils/telegram'
import PostMenu from './PostMenu'
import CommentsModal from './CommentsModal'
import './PostCard.css'
const TAG_COLORS = {
@ -23,8 +21,6 @@ export default function PostCard({ post, currentUser, onUpdate }) {
const navigate = useNavigate()
const [liked, setLiked] = useState(post.likes.includes(currentUser.id))
const [likesCount, setLikesCount] = useState(post.likes.length)
const [showMenu, setShowMenu] = useState(false)
const [showComments, setShowComments] = useState(false)
const [currentImageIndex, setCurrentImageIndex] = useState(0)
// Поддержка и старого поля imageUrl и нового images
@ -87,7 +83,7 @@ export default function PostCard({ post, currentUser, onUpdate }) {
</div>
</div>
<button className="menu-btn" onClick={() => setShowMenu(true)}>
<button className="menu-btn" onClick={() => navigate(`/post/${post._id}/menu`)}>
<MoreVertical size={20} />
</button>
</div>
@ -160,30 +156,11 @@ export default function PostCard({ post, currentUser, onUpdate }) {
<span>{likesCount}</span>
</button>
<button className="action-btn" onClick={() => setShowComments(true)}>
<button className="action-btn" onClick={() => navigate(`/post/${post._id}/comments`)}>
<MessageCircle size={20} stroke="currentColor" />
<span>{post.comments.length}</span>
</button>
</div>
{/* Меню поста */}
{showMenu && (
<PostMenu
post={post}
currentUser={currentUser}
onClose={() => setShowMenu(false)}
onDelete={handleDelete}
/>
)}
{/* Комментарии */}
{showComments && (
<CommentsModal
post={post}
onClose={() => setShowComments(false)}
onUpdate={onUpdate}
/>
)}
</div>
)
}

View File

@ -0,0 +1,233 @@
.comments-page {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: var(--bg-secondary);
overflow: hidden;
}
.page-header {
padding: 16px;
border-bottom: 1px solid var(--divider-color);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
background: var(--bg-secondary);
z-index: 100;
}
.page-header h2 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.back-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bg-primary);
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
}
.back-btn svg {
stroke: currentColor;
}
.loading-state,
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
gap: 16px;
}
.loading-state p,
.empty-state p {
color: var(--text-secondary);
font-size: 16px;
}
/* Превью поста */
.post-preview {
padding: 16px;
border-bottom: 1px solid var(--divider-color);
background: var(--bg-secondary);
}
.preview-author {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
.preview-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.preview-name {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
.preview-username {
font-size: 13px;
color: var(--text-secondary);
}
.preview-content {
font-size: 15px;
line-height: 1.5;
color: var(--text-primary);
margin-bottom: 12px;
white-space: pre-wrap;
}
.preview-images {
display: flex;
flex-direction: column;
gap: 8px;
}
.preview-images img {
width: 100%;
border-radius: 12px;
max-height: 400px;
object-fit: contain;
background: var(--bg-primary);
}
.comments-list {
flex: 1;
overflow-y: auto;
padding: 16px;
padding-bottom: 100px;
display: flex;
flex-direction: column;
gap: 16px;
}
.empty-comments {
padding: 60px 20px;
text-align: center;
}
.empty-comments p {
color: var(--text-primary);
font-size: 16px;
margin-bottom: 8px;
}
.empty-comments span {
color: var(--text-secondary);
font-size: 14px;
}
.comment-item {
display: flex;
gap: 12px;
}
.comment-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
object-fit: cover;
}
.comment-content {
flex: 1;
}
.comment-header {
display: flex;
gap: 8px;
margin-bottom: 4px;
}
.comment-author {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.comment-time {
font-size: 12px;
color: var(--text-secondary);
}
.comment-text {
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
}
.comment-form {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 12px 16px;
padding-bottom: calc(12px + 80px); /* Отступ для навигации */
background: var(--bg-secondary);
border-top: 1px solid var(--divider-color);
display: flex;
gap: 8px;
z-index: 1000;
}
.comment-form input {
flex: 1;
padding: 12px 16px;
border-radius: 24px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 15px;
border: none;
}
.send-btn {
width: 44px;
height: 44px;
border-radius: 50%;
background: #1C1C1E;
color: white;
display: flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
flex-shrink: 0;
}
.send-btn svg {
stroke: white;
}
.send-btn:disabled {
opacity: 0.5;
}
[data-theme="dark"] .send-btn {
background: #FFFFFF;
color: #000000;
}
[data-theme="dark"] .send-btn svg {
stroke: #000000;
}

View File

@ -0,0 +1,198 @@
import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { ArrowLeft, Send } from 'lucide-react'
import { getPosts, commentPost } from '../utils/api'
import { hapticFeedback } from '../utils/telegram'
import './CommentsPage.css'
export default function CommentsPage({ user }) {
const navigate = useNavigate()
const { postId } = useParams()
const [post, setPost] = useState(null)
const [comment, setComment] = useState('')
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [comments, setComments] = useState([])
useEffect(() => {
loadPost()
}, [postId])
const loadPost = async () => {
try {
setLoading(true)
const posts = await getPosts()
const foundPost = posts.posts.find(p => p._id === postId)
if (foundPost) {
setPost(foundPost)
setComments(foundPost.comments || [])
}
} catch (error) {
console.error('Ошибка загрузки поста:', error)
} finally {
setLoading(false)
}
}
const handleSubmit = async () => {
if (!comment.trim()) return
try {
setSubmitting(true)
hapticFeedback('light')
const result = await commentPost(postId, comment)
setComments(result.comments)
setComment('')
hapticFeedback('success')
// Обновить пост
await loadPost()
} catch (error) {
console.error('Ошибка добавления комментария:', error)
hapticFeedback('error')
} finally {
setSubmitting(false)
}
}
const formatDate = (date) => {
const d = new Date(date)
const now = new Date()
const diff = Math.floor((now - d) / 1000) // секунды
if (diff < 60) return 'только что'
if (diff < 3600) return `${Math.floor(diff / 60)} мин`
if (diff < 86400) return `${Math.floor(diff / 3600)} ч`
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })
}
if (loading) {
return (
<div className="comments-page">
<div className="page-header">
<button className="back-btn" onClick={() => navigate(-1)}>
<ArrowLeft size={24} />
</button>
<h2>Комментарии</h2>
<div style={{ width: 40 }} />
</div>
<div className="loading-state">
<div className="spinner" />
<p>Загрузка...</p>
</div>
</div>
)
}
if (!post) {
return (
<div className="comments-page">
<div className="page-header">
<button className="back-btn" onClick={() => navigate(-1)}>
<ArrowLeft size={24} />
</button>
<h2>Комментарии</h2>
<div style={{ width: 40 }} />
</div>
<div className="empty-state">
<p>Пост не найден</p>
</div>
</div>
)
}
// Поддержка и старого поля imageUrl и нового images
const images = post.images && post.images.length > 0 ? post.images : (post.imageUrl ? [post.imageUrl] : [])
return (
<div className="comments-page">
{/* Хедер */}
<div className="page-header">
<button className="back-btn" onClick={() => navigate(-1)}>
<ArrowLeft size={24} />
</button>
<h2>Комментарии</h2>
<div style={{ width: 40 }} />
</div>
{/* Пост */}
<div className="post-preview">
<div className="preview-author">
<img
src={post.author.photoUrl || '/default-avatar.png'}
alt={post.author.username}
className="preview-avatar"
/>
<div>
<div className="preview-name">
{post.author.firstName} {post.author.lastName}
</div>
<div className="preview-username">@{post.author.username}</div>
</div>
</div>
{post.content && (
<div className="preview-content">{post.content}</div>
)}
{images.length > 0 && (
<div className="preview-images">
{images.map((img, index) => (
<img key={index} src={img} alt={`Post ${index + 1}`} />
))}
</div>
)}
</div>
{/* Список комментариев */}
<div className="comments-list">
{comments.length === 0 ? (
<div className="empty-comments">
<p>Пока нет комментариев</p>
<span>Будьте первым!</span>
</div>
) : (
comments.map((c, index) => (
<div key={index} className="comment-item fade-in">
<img
src={c.author.photoUrl || '/default-avatar.png'}
alt={c.author.username}
className="comment-avatar"
/>
<div className="comment-content">
<div className="comment-header">
<span className="comment-author">
{c.author.firstName} {c.author.lastName}
</span>
<span className="comment-time">{formatDate(c.createdAt)}</span>
</div>
<p className="comment-text">{c.content}</p>
</div>
</div>
))
)}
</div>
{/* Форма добавления комментария */}
<div className="comment-form">
<input
type="text"
placeholder="Написать комментарий..."
value={comment}
onChange={e => setComment(e.target.value)}
onKeyPress={e => e.key === 'Enter' && handleSubmit()}
maxLength={500}
/>
<button
onClick={handleSubmit}
disabled={submitting || !comment.trim()}
className="send-btn"
>
<Send size={20} />
</button>
</div>
</div>
)
}

View File

@ -0,0 +1,190 @@
.post-menu-page {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: var(--bg-secondary);
overflow: hidden;
}
.page-header {
padding: 16px;
border-bottom: 1px solid var(--divider-color);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
background: var(--bg-secondary);
z-index: 100;
}
.page-header h2 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.back-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bg-primary);
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
}
.back-btn svg {
stroke: currentColor;
}
.loading-state,
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
gap: 16px;
}
.loading-state p,
.empty-state p {
color: var(--text-secondary);
font-size: 16px;
}
/* Превью поста */
.post-preview {
padding: 16px;
border-bottom: 1px solid var(--divider-color);
background: var(--bg-secondary);
}
.preview-author {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
.preview-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.preview-name {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
.preview-username {
font-size: 13px;
color: var(--text-secondary);
}
.preview-content {
font-size: 15px;
line-height: 1.5;
color: var(--text-primary);
margin-bottom: 12px;
white-space: pre-wrap;
}
.preview-images {
display: flex;
flex-direction: column;
gap: 8px;
}
.preview-images img {
width: 100%;
border-radius: 12px;
max-height: 400px;
object-fit: contain;
background: var(--bg-primary);
}
.menu-items {
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.menu-item {
width: 100%;
padding: 16px;
background: var(--bg-primary);
border: none;
display: flex;
align-items: center;
gap: 12px;
color: var(--text-primary);
font-size: 16px;
font-weight: 500;
border-radius: 12px;
cursor: pointer;
}
.menu-item svg {
stroke: currentColor;
}
.menu-item:active {
opacity: 0.7;
transform: scale(0.98);
}
.menu-item.danger {
color: #FF3B30;
}
.submit-btn {
padding: 8px 16px;
border-radius: 20px;
background: #1C1C1E;
color: white;
font-size: 14px;
font-weight: 600;
border: none;
cursor: pointer;
}
.submit-btn:disabled {
opacity: 0.5;
}
[data-theme="dark"] .submit-btn {
background: #FFFFFF;
color: #000000;
}
[data-theme="dark"] .submit-btn:disabled {
opacity: 0.5;
}
.report-body {
flex: 1;
padding: 16px;
}
.report-body textarea {
width: 100%;
height: 200px;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 12px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 15px;
line-height: 1.5;
resize: none;
}

View File

@ -0,0 +1,198 @@
import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { ArrowLeft, Trash2, Flag } from 'lucide-react'
import { getPosts, reportPost, deletePost } from '../utils/api'
import { hapticFeedback, showConfirm } from '../utils/telegram'
import './PostMenuPage.css'
export default function PostMenuPage({ user }) {
const navigate = useNavigate()
const { postId } = useParams()
const [post, setPost] = useState(null)
const [loading, setLoading] = useState(true)
const [showReportModal, setShowReportModal] = useState(false)
const [reportReason, setReportReason] = useState('')
const [submitting, setSubmitting] = useState(false)
useEffect(() => {
loadPost()
}, [postId])
const loadPost = async () => {
try {
setLoading(true)
const posts = await getPosts()
const foundPost = posts.posts.find(p => p._id === postId)
if (foundPost) {
setPost(foundPost)
}
} catch (error) {
console.error('Ошибка загрузки поста:', error)
} finally {
setLoading(false)
}
}
const handleDelete = async () => {
const confirmed = await showConfirm('Удалить этот пост?')
if (confirmed) {
try {
hapticFeedback('light')
await deletePost(postId)
hapticFeedback('success')
navigate(-1)
} catch (error) {
console.error('Ошибка удаления:', error)
hapticFeedback('error')
}
}
}
const handleReport = async () => {
if (!reportReason.trim()) {
alert('Укажите причину жалобы')
return
}
try {
setSubmitting(true)
hapticFeedback('light')
await reportPost(postId, reportReason)
hapticFeedback('success')
alert('Жалоба отправлена')
navigate(-1)
} catch (error) {
console.error('Ошибка отправки жалобы:', error)
hapticFeedback('error')
alert('Ошибка отправки жалобы')
} finally {
setSubmitting(false)
}
}
if (loading) {
return (
<div className="post-menu-page">
<div className="page-header">
<button className="back-btn" onClick={() => navigate(-1)}>
<ArrowLeft size={24} />
</button>
<h2>Действия</h2>
<div style={{ width: 40 }} />
</div>
<div className="loading-state">
<div className="spinner" />
<p>Загрузка...</p>
</div>
</div>
)
}
if (!post) {
return (
<div className="post-menu-page">
<div className="page-header">
<button className="back-btn" onClick={() => navigate(-1)}>
<ArrowLeft size={24} />
</button>
<h2>Действия</h2>
<div style={{ width: 40 }} />
</div>
<div className="empty-state">
<p>Пост не найден</p>
</div>
</div>
)
}
if (showReportModal) {
return (
<div className="post-menu-page">
<div className="page-header">
<button className="back-btn" onClick={() => setShowReportModal(false)}>
<ArrowLeft size={24} />
</button>
<h2>Пожаловаться</h2>
<button
className="submit-btn"
onClick={handleReport}
disabled={submitting || !reportReason.trim()}
>
{submitting ? 'Отправка...' : 'Отправить'}
</button>
</div>
<div className="report-body">
<textarea
placeholder="Опишите причину жалобы..."
value={reportReason}
onChange={e => setReportReason(e.target.value)}
maxLength={500}
autoFocus
/>
</div>
</div>
)
}
return (
<div className="post-menu-page">
<div className="page-header">
<button className="back-btn" onClick={() => navigate(-1)}>
<ArrowLeft size={24} />
</button>
<h2>Действия</h2>
<div style={{ width: 40 }} />
</div>
{/* Пост */}
<div className="post-preview">
<div className="preview-author">
<img
src={post.author.photoUrl || '/default-avatar.png'}
alt={post.author.username}
className="preview-avatar"
/>
<div>
<div className="preview-name">
{post.author.firstName} {post.author.lastName}
</div>
<div className="preview-username">@{post.author.username}</div>
</div>
</div>
{post.content && (
<div className="preview-content">{post.content}</div>
)}
{(post.images && post.images.length > 0) || post.imageUrl ? (
<div className="preview-images">
{post.images && post.images.length > 0 ? (
post.images.map((img, index) => (
<img key={index} src={img} alt={`Post ${index + 1}`} />
))
) : (
<img src={post.imageUrl} alt="Post" />
)}
</div>
) : null}
</div>
{/* Меню */}
<div className="menu-items">
{(post.author._id === user.id) || (user.role === 'moderator' || user.role === 'admin') ? (
<button className="menu-item danger" onClick={handleDelete}>
<Trash2 size={20} />
<span>Удалить пост</span>
</button>
) : (
<button className="menu-item" onClick={() => setShowReportModal(true)}>
<Flag size={20} />
<span>Пожаловаться</span>
</button>
)}
</div>
</div>
)
}

65
🔧_GELBOORU_API.txt Normal file
View File

@ -0,0 +1,65 @@
╔═══════════════════════════════════════════════════════════════════╗
║ ║
║ 🔧 GELBOORU API КЛЮЧ ДОБАВЛЕН! 🔧 ║
║ ║
╚═══════════════════════════════════════════════════════════════════╝
ИСПРАВЛЕНО:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 1. Добавлен Gelbooru API ключ в конфиг
• API Key: 638e2433d451fc02e848811acdafdce08317073c01ed78e38139115c19fe04afa367f736726514ef1337565d4c05b3cbe2c81125c424301e90d29d1f7f4cceff
• User ID: 1844464
✅ 2. Обновлены запросы к Gelbooru API
• Поиск постов теперь использует api_key и user_id
• Автокомплит тегов теперь использует api_key и user_id
• Согласно документации: https://gelbooru.com/index.php?page=wiki&s=view&id=18780
✅ 3. Запросы не будут ограничены
• API ключ позволяет избежать throttling
• Более стабильная работа API
ИЗМЕНЕННЫЕ ФАЙЛЫ:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Backend:
• backend/config/index.js
• backend/routes/search.js
ОБНОВЛЕНИЕ (2 файла):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
cd /Users/glpshchn/Desktop/nakama
# Backend
scp backend/config/index.js backend/routes/search.js root@ваш_IP:/var/www/nakama/backend/
scp backend/routes/search.js root@ваш_IP:/var/www/nakama/backend/routes/
# На сервере
ssh root@ваш_IP "cd /var/www/nakama/backend && pm2 restart nakama-backend"
ЧТО ИСПРАВЛЕНО:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. ✅ Gelbooru API теперь использует аутентификацию
2. ✅ Запросы не будут ограничены (throttling)
3. ✅ Более стабильная работа поиска в Gelbooru
ПРИМЕЧАНИЕ:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
API ключ можно также добавить в переменные окружения:
• GELBOORU_API_KEY=638e2433d451fc02e848811acdafdce08317073c01ed78e38139115c19fe04afa367f736726514ef1337565d4c05b3cbe2c81125c424301e90d29d1f7f4cceff
• GELBOORU_USER_ID=1844464
Если не указаны, будут использоваться значения по умолчанию из конфига.
2 минуты

View File

@ -0,0 +1,82 @@
╔═══════════════════════════════════════════════════════════════════╗
║ ║
║ 🔧 СТРАНИЦЫ ВМЕСТО МОДАЛОК + ИСПРАВЛЕН БОТ 🔧 ║
║ ║
╚═══════════════════════════════════════════════════════════════════╝
ИСПРАВЛЕНО:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 1. Проблема с ботом (botundefined в URL)
• Добавлена проверка на наличие TELEGRAM_BOT_TOKEN
• Преобразование относительных URL в полные для Telegram
• Улучшена обработка ошибок
✅ 2. Создана страница CommentsPage
• Отдельная страница для комментариев
• Пост дублируется на странице
• Кнопка "Назад" для возврата
• Нет проблем с прыганием!
✅ 3. Создана страница PostMenuPage
• Отдельная страница для меню поста
• Пост дублируется на странице
• Кнопка "Назад" для возврата
• Нет проблем с прыганием!
✅ 4. Добавлены маршруты в App.jsx
• /post/:postId/comments - страница комментариев
• /post/:postId/menu - страница меню поста
✅ 5. Обновлен PostCard.jsx
• Навигация на страницы вместо модальных окон
• Убраны импорты PostMenu и CommentsModal
ИЗМЕНЕННЫЕ ФАЙЛЫ:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Backend:
• backend/bot.js
Frontend:
• frontend/src/App.jsx
• frontend/src/components/PostCard.jsx
• frontend/src/pages/CommentsPage.jsx (новый)
• frontend/src/pages/CommentsPage.css (новый)
• frontend/src/pages/PostMenuPage.jsx (новый)
• frontend/src/pages/PostMenuPage.css (новый)
ОБНОВЛЕНИЕ (8 файлов):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
cd /Users/glpshchn/Desktop/nakama
# Backend
scp backend/bot.js root@ваш_IP:/var/www/nakama/backend/
# Frontend
scp frontend/src/App.jsx frontend/src/components/PostCard.jsx root@ваш_IP:/var/www/nakama/frontend/src/
scp frontend/src/components/PostCard.jsx root@ваш_IP:/var/www/nakama/frontend/src/components/
scp frontend/src/pages/CommentsPage.jsx frontend/src/pages/CommentsPage.css root@ваш_IP:/var/www/nakama/frontend/src/pages/
scp frontend/src/pages/PostMenuPage.jsx frontend/src/pages/PostMenuPage.css root@ваш_IP:/var/www/nakama/frontend/src/pages/
# На сервере
ssh root@ваш_IP "cd /var/www/nakama/frontend && npm run build"
ssh root@ваш_IP "cd /var/www/nakama/backend && pm2 restart nakama-backend"
ЧТО ИСПРАВЛЕНО:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. ✅ Бот больше не выдает ошибку 404
2. ✅ Комментарии на отдельной странице - НЕ ПРЫГАЮТ!
3. ✅ Меню поста на отдельной странице - НЕ ПРЫГАЕТ!
4. ✅ Пост дублируется на обеих страницах
5. ✅ Кнопка "Назад" работает правильно
5 минут