Update files
This commit is contained in:
parent
d90f6088d2
commit
a678b129c8
|
|
@ -2,14 +2,33 @@
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const config = require('./config');
|
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) {
|
async function sendPhotoToUser(userId, photoUrl, caption) {
|
||||||
|
if (!TELEGRAM_API) {
|
||||||
|
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
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`, {
|
const response = await axios.post(`${TELEGRAM_API}/sendPhoto`, {
|
||||||
chat_id: userId,
|
chat_id: userId,
|
||||||
photo: photoUrl,
|
photo: finalPhotoUrl,
|
||||||
caption: caption || '',
|
caption: caption || '',
|
||||||
parse_mode: 'HTML'
|
parse_mode: 'HTML'
|
||||||
});
|
});
|
||||||
|
|
@ -23,6 +42,10 @@ async function sendPhotoToUser(userId, photoUrl, caption) {
|
||||||
|
|
||||||
// Отправить несколько фото группой (до 10 штук)
|
// Отправить несколько фото группой (до 10 штук)
|
||||||
async function sendPhotosToUser(userId, photos) {
|
async function sendPhotosToUser(userId, photos) {
|
||||||
|
if (!TELEGRAM_API) {
|
||||||
|
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Telegram поддерживает до 10 фото в одной группе
|
// Telegram поддерживает до 10 фото в одной группе
|
||||||
const batches = [];
|
const batches = [];
|
||||||
|
|
@ -31,14 +54,23 @@ async function sendPhotosToUser(userId, photos) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
const baseUrl = process.env.FRONTEND_URL || process.env.API_URL || 'https://nakama.glpshchn.ru';
|
||||||
|
|
||||||
for (const batch of batches) {
|
for (const batch of batches) {
|
||||||
const media = batch.map((photo, index) => ({
|
const media = batch.map((photo, index) => {
|
||||||
type: 'photo',
|
// Преобразуем относительные URL в полные
|
||||||
media: photo.url,
|
let photoUrl = photo.url;
|
||||||
caption: index === 0 ? `<b>Из NakamaSpace</b>\n${batch.length} фото` : undefined,
|
if (photoUrl.startsWith('/')) {
|
||||||
parse_mode: 'HTML'
|
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`, {
|
const response = await axios.post(`${TELEGRAM_API}/sendMediaGroup`, {
|
||||||
chat_id: userId,
|
chat_id: userId,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ module.exports = {
|
||||||
// Telegram
|
// Telegram
|
||||||
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN,
|
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN,
|
||||||
|
|
||||||
|
// Gelbooru API
|
||||||
|
gelbooruApiKey: process.env.GELBOORU_API_KEY || '638e2433d451fc02e848811acdafdce08317073c01ed78e38139115c19fe04afa367f736726514ef1337565d4c05b3cbe2c81125c424301e90d29d1f7f4cceff',
|
||||||
|
gelbooruUserId: process.env.GELBOORU_USER_ID || '1844464',
|
||||||
|
|
||||||
// Frontend URL
|
// Frontend URL
|
||||||
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:5173',
|
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { authenticate } = require('../middleware/auth');
|
const { authenticate } = require('../middleware/auth');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
// Функция для создания прокси URL
|
// Функция для создания прокси URL
|
||||||
function createProxyUrl(originalUrl) {
|
function createProxyUrl(originalUrl) {
|
||||||
|
|
@ -117,7 +118,9 @@ router.get('/anime', authenticate, async (req, res) => {
|
||||||
json: 1,
|
json: 1,
|
||||||
tags: query,
|
tags: query,
|
||||||
limit,
|
limit,
|
||||||
pid: page
|
pid: page,
|
||||||
|
api_key: config.gelbooruApiKey,
|
||||||
|
user_id: config.gelbooruUserId
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
'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,
|
json: 1,
|
||||||
name_pattern: `${query}%`,
|
name_pattern: `${query}%`,
|
||||||
orderby: 'count',
|
orderby: 'count',
|
||||||
limit: 10
|
limit: 10,
|
||||||
|
api_key: config.gelbooruApiKey,
|
||||||
|
user_id: config.gelbooruUserId
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import Search from './pages/Search'
|
||||||
import Notifications from './pages/Notifications'
|
import Notifications from './pages/Notifications'
|
||||||
import Profile from './pages/Profile'
|
import Profile from './pages/Profile'
|
||||||
import UserProfile from './pages/UserProfile'
|
import UserProfile from './pages/UserProfile'
|
||||||
|
import CommentsPage from './pages/CommentsPage'
|
||||||
|
import PostMenuPage from './pages/PostMenuPage'
|
||||||
import './styles/index.css'
|
import './styles/index.css'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
@ -92,6 +94,8 @@ function App() {
|
||||||
<Route path="notifications" element={<Notifications user={user} />} />
|
<Route path="notifications" element={<Notifications user={user} />} />
|
||||||
<Route path="profile" element={<Profile user={user} setUser={setUser} />} />
|
<Route path="profile" element={<Profile user={user} setUser={setUser} />} />
|
||||||
<Route path="user/:id" element={<UserProfile currentUser={user} />} />
|
<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>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight } from 'lucide-react'
|
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 { hapticFeedback, showConfirm } from '../utils/telegram'
|
||||||
import PostMenu from './PostMenu'
|
|
||||||
import CommentsModal from './CommentsModal'
|
|
||||||
import './PostCard.css'
|
import './PostCard.css'
|
||||||
|
|
||||||
const TAG_COLORS = {
|
const TAG_COLORS = {
|
||||||
|
|
@ -23,8 +21,6 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [liked, setLiked] = useState(post.likes.includes(currentUser.id))
|
const [liked, setLiked] = useState(post.likes.includes(currentUser.id))
|
||||||
const [likesCount, setLikesCount] = useState(post.likes.length)
|
const [likesCount, setLikesCount] = useState(post.likes.length)
|
||||||
const [showMenu, setShowMenu] = useState(false)
|
|
||||||
const [showComments, setShowComments] = useState(false)
|
|
||||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||||
|
|
||||||
// Поддержка и старого поля imageUrl и нового images
|
// Поддержка и старого поля imageUrl и нового images
|
||||||
|
|
@ -87,7 +83,7 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="menu-btn" onClick={() => setShowMenu(true)}>
|
<button className="menu-btn" onClick={() => navigate(`/post/${post._id}/menu`)}>
|
||||||
<MoreVertical size={20} />
|
<MoreVertical size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -160,30 +156,11 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
<span>{likesCount}</span>
|
<span>{likesCount}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button className="action-btn" onClick={() => setShowComments(true)}>
|
<button className="action-btn" onClick={() => navigate(`/post/${post._id}/comments`)}>
|
||||||
<MessageCircle size={20} stroke="currentColor" />
|
<MessageCircle size={20} stroke="currentColor" />
|
||||||
<span>{post.comments.length}</span>
|
<span>{post.comments.length}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Меню поста */}
|
|
||||||
{showMenu && (
|
|
||||||
<PostMenu
|
|
||||||
post={post}
|
|
||||||
currentUser={currentUser}
|
|
||||||
onClose={() => setShowMenu(false)}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Комментарии */}
|
|
||||||
{showComments && (
|
|
||||||
<CommentsModal
|
|
||||||
post={post}
|
|
||||||
onClose={() => setShowComments(false)}
|
|
||||||
onUpdate={onUpdate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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 минуты
|
||||||
|
|
||||||
|
|
@ -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 минут
|
||||||
|
|
||||||
Loading…
Reference in New Issue