Update files
This commit is contained in:
parent
7ae1b2b8d8
commit
66149df2a5
|
|
@ -13,7 +13,7 @@ const NotificationSchema = new mongoose.Schema({
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ['follow', 'like', 'comment', 'mention'],
|
enum: ['follow', 'like', 'comment', 'mention', 'new_post'],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
post: {
|
post: {
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,19 @@ router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploa
|
||||||
}));
|
}));
|
||||||
await Notification.insertMany(notifications);
|
await Notification.insertMany(notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Создать уведомления для подписчиков о новом посте
|
||||||
|
const User = require('../models/User');
|
||||||
|
const author = await User.findById(req.user._id).select('followers');
|
||||||
|
if (author && author.followers && author.followers.length > 0) {
|
||||||
|
const newPostNotifications = author.followers.map(followerId => ({
|
||||||
|
recipient: followerId,
|
||||||
|
sender: req.user._id,
|
||||||
|
type: 'new_post',
|
||||||
|
post: post._id
|
||||||
|
}));
|
||||||
|
await Notification.insertMany(newPostNotifications);
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json({ post });
|
res.status(201).json({ post });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ router.get('/:id', authenticate, async (req, res) => {
|
||||||
bio: user.bio,
|
bio: user.bio,
|
||||||
followersCount: user.followers.length,
|
followersCount: user.followers.length,
|
||||||
followingCount: user.following.length,
|
followingCount: user.following.length,
|
||||||
|
followers: user.followers,
|
||||||
|
following: user.following,
|
||||||
isFollowing,
|
isFollowing,
|
||||||
createdAt: user.createdAt
|
createdAt: user.createdAt
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||||
import { X, Send } from 'lucide-react'
|
import { X, Send } from 'lucide-react'
|
||||||
import { commentPost } from '../utils/api'
|
import { commentPost } from '../utils/api'
|
||||||
import { hapticFeedback } from '../utils/telegram'
|
import { hapticFeedback } from '../utils/telegram'
|
||||||
|
import { decodeHtmlEntities } from '../utils/htmlEntities'
|
||||||
import './CommentsModal.css'
|
import './CommentsModal.css'
|
||||||
|
|
||||||
export default function CommentsModal({ post, onClose, onUpdate }) {
|
export default function CommentsModal({ post, onClose, onUpdate }) {
|
||||||
|
|
@ -79,12 +80,12 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{post.content && (
|
{post.content && (
|
||||||
<div className="preview-content">{post.content}</div>
|
<div className="preview-content">{decodeHtmlEntities(post.content)}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{post.imageUrl && (
|
{((post.images && post.images.length > 0) || post.imageUrl) && (
|
||||||
<div className="preview-image">
|
<div className="preview-image">
|
||||||
<img src={post.imageUrl} alt="Post" />
|
<img src={post.images?.[0] || post.imageUrl} alt="Post" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -115,7 +116,7 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
|
||||||
</span>
|
</span>
|
||||||
<span className="comment-time">{formatDate(c.createdAt)}</span>
|
<span className="comment-time">{formatDate(c.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="comment-text">{c.content}</p>
|
<p className="comment-text">{decodeHtmlEntities(c.content)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
/* Overlay */
|
||||||
|
.follow-list-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.follow-list-modal {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.follow-list-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-list-header h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-list-header .close-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-list-header .close-btn:active {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.follow-list-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
font-size: 15px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Users List */
|
||||||
|
.users-list {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item:active {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-username {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Follow Button */
|
||||||
|
.follow-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--button-accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-btn:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-btn.following {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-btn span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { X, UserPlus, UserMinus } from 'lucide-react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { followUser, unfollowUser } from '../utils/api'
|
||||||
|
import { hapticFeedback } from '../utils/telegram'
|
||||||
|
import './FollowListModal.css'
|
||||||
|
|
||||||
|
export default function FollowListModal({ users, title, onClose, currentUser }) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [userStates, setUserStates] = useState(
|
||||||
|
users.reduce((acc, user) => {
|
||||||
|
acc[user._id] = {
|
||||||
|
isFollowing: currentUser?.following?.some(f => f._id === user._id || f === user._id) || false
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleOverlayClick = (e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUserClick = (userId) => {
|
||||||
|
hapticFeedback('light')
|
||||||
|
onClose()
|
||||||
|
navigate(`/user/${userId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFollowToggle = async (userId, e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
try {
|
||||||
|
hapticFeedback('light')
|
||||||
|
const isCurrentlyFollowing = userStates[userId]?.isFollowing || false
|
||||||
|
|
||||||
|
if (isCurrentlyFollowing) {
|
||||||
|
await unfollowUser(userId)
|
||||||
|
setUserStates(prev => ({
|
||||||
|
...prev,
|
||||||
|
[userId]: { isFollowing: false }
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
await followUser(userId)
|
||||||
|
setUserStates(prev => ({
|
||||||
|
...prev,
|
||||||
|
[userId]: { isFollowing: true }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
hapticFeedback('success')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка подписки:', error)
|
||||||
|
hapticFeedback('error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="follow-list-modal-overlay" onClick={handleOverlayClick}>
|
||||||
|
<div className="follow-list-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
{/* Хедер */}
|
||||||
|
<div className="follow-list-header">
|
||||||
|
<button className="close-btn" onClick={onClose}>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<div style={{ width: 40 }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Список пользователей */}
|
||||||
|
<div className="follow-list-content">
|
||||||
|
{users.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<p>Пока никого нет</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="users-list">
|
||||||
|
{users.map((user) => {
|
||||||
|
const isOwnProfile = user._id === currentUser?.id
|
||||||
|
const isFollowing = userStates[user._id]?.isFollowing || false
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={user._id}
|
||||||
|
className="user-item"
|
||||||
|
onClick={() => handleUserClick(user._id)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={user.photoUrl || '/default-avatar.png'}
|
||||||
|
alt={user.username || user.firstName || 'User'}
|
||||||
|
className="user-avatar"
|
||||||
|
onError={(e) => { e.target.src = '/default-avatar.png' }}
|
||||||
|
/>
|
||||||
|
<div className="user-info">
|
||||||
|
<div className="user-name">
|
||||||
|
{user.firstName || ''} {user.lastName || ''}
|
||||||
|
{!user.firstName && !user.lastName && 'Пользователь'}
|
||||||
|
</div>
|
||||||
|
<div className="user-username">@{user.username || user.firstName || 'user'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isOwnProfile && (
|
||||||
|
<button
|
||||||
|
className={`follow-btn ${isFollowing ? 'following' : ''}`}
|
||||||
|
onClick={(e) => handleFollowToggle(user._id, e)}
|
||||||
|
>
|
||||||
|
{isFollowing ? (
|
||||||
|
<>
|
||||||
|
<UserMinus size={16} />
|
||||||
|
<span>Отписаться</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<UserPlus size={16} />
|
||||||
|
<span>Подписаться</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -319,23 +319,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullview-content {
|
.fullview-content {
|
||||||
flex: 1;
|
position: fixed;
|
||||||
|
top: 68px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 68px;
|
|
||||||
margin-bottom: 80px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullview-content img {
|
.fullview-content img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: calc(100vh - 148px);
|
max-height: 100%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
display: block;
|
display: block;
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullview-nav-btn {
|
.fullview-nav-btn {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import { useNavigate } from 'react-router-dom'
|
||||||
import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight, Download, Send, X, ZoomIn, Share2 } from 'lucide-react'
|
import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight, Download, Send, X, ZoomIn, Share2 } from 'lucide-react'
|
||||||
import { likePost, deletePost, sendPhotoToTelegram } from '../utils/api'
|
import { likePost, deletePost, sendPhotoToTelegram } from '../utils/api'
|
||||||
import { hapticFeedback, showConfirm, openTelegramLink } from '../utils/telegram'
|
import { hapticFeedback, showConfirm, openTelegramLink } from '../utils/telegram'
|
||||||
|
import { decodeHtmlEntities } from '../utils/htmlEntities'
|
||||||
|
import CommentsModal from './CommentsModal'
|
||||||
|
import PostMenu from './PostMenu'
|
||||||
import './PostCard.css'
|
import './PostCard.css'
|
||||||
|
|
||||||
const TAG_COLORS = {
|
const TAG_COLORS = {
|
||||||
|
|
@ -23,6 +26,8 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
const [likesCount, setLikesCount] = useState(post.likes?.length || 0)
|
const [likesCount, setLikesCount] = useState(post.likes?.length || 0)
|
||||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||||
const [showFullView, setShowFullView] = useState(false)
|
const [showFullView, setShowFullView] = useState(false)
|
||||||
|
const [showComments, setShowComments] = useState(false)
|
||||||
|
const [showMenu, setShowMenu] = useState(false)
|
||||||
|
|
||||||
// Проверка на существование автора
|
// Проверка на существование автора
|
||||||
if (!post.author) {
|
if (!post.author) {
|
||||||
|
|
@ -164,7 +169,7 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
className="menu-btn"
|
className="menu-btn"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
navigate(`/post/${post._id}/menu`)
|
setShowMenu(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MoreVertical size={20} />
|
<MoreVertical size={20} />
|
||||||
|
|
@ -174,7 +179,7 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
{/* Контент */}
|
{/* Контент */}
|
||||||
{post.content && (
|
{post.content && (
|
||||||
<div className="post-content">
|
<div className="post-content">
|
||||||
{post.content}
|
{decodeHtmlEntities(post.content)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -237,7 +242,7 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
className="action-btn"
|
className="action-btn"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
navigate(`/post/${post._id}/comments`)
|
setShowComments(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MessageCircle size={20} stroke="currentColor" />
|
<MessageCircle size={20} stroke="currentColor" />
|
||||||
|
|
@ -254,27 +259,6 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
>
|
>
|
||||||
<Share2 size={20} stroke="currentColor" />
|
<Share2 size={20} stroke="currentColor" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{images.length > 0 && (
|
|
||||||
<button
|
|
||||||
className="action-btn"
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
try {
|
|
||||||
hapticFeedback('light')
|
|
||||||
const imageUrl = images[currentImageIndex] || post.imageUrl
|
|
||||||
await sendPhotoToTelegram(imageUrl)
|
|
||||||
hapticFeedback('success')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка отправки фото:', error)
|
|
||||||
hapticFeedback('error')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title="Отправить фото в Telegram"
|
|
||||||
>
|
|
||||||
<Send size={20} stroke="currentColor" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fullview модал */}
|
{/* Fullview модал */}
|
||||||
|
|
@ -333,6 +317,28 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Модалка комментариев */}
|
||||||
|
{showComments && (
|
||||||
|
<CommentsModal
|
||||||
|
post={post}
|
||||||
|
onClose={() => setShowComments(false)}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Модалка меню */}
|
||||||
|
{showMenu && (
|
||||||
|
<PostMenu
|
||||||
|
post={post}
|
||||||
|
currentUser={currentUser}
|
||||||
|
onClose={() => setShowMenu(false)}
|
||||||
|
onDelete={async () => {
|
||||||
|
await handleDelete()
|
||||||
|
setShowMenu(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { ArrowLeft, Send, Edit, Trash2 } from 'lucide-react'
|
import { ArrowLeft, Send, Edit, Trash2 } from 'lucide-react'
|
||||||
import { getPosts, commentPost, editComment, deleteComment } from '../utils/api'
|
import { getPosts, commentPost, editComment, deleteComment } from '../utils/api'
|
||||||
import { hapticFeedback, showConfirm } from '../utils/telegram'
|
import { hapticFeedback, showConfirm } from '../utils/telegram'
|
||||||
|
import { decodeHtmlEntities } from '../utils/htmlEntities'
|
||||||
import './CommentsPage.css'
|
import './CommentsPage.css'
|
||||||
|
|
||||||
export default function CommentsPage({ user }) {
|
export default function CommentsPage({ user }) {
|
||||||
|
|
@ -137,7 +138,7 @@ export default function CommentsPage({ user }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{post.content && (
|
{post.content && (
|
||||||
<div className="preview-content">{post.content}</div>
|
<div className="preview-content">{decodeHtmlEntities(post.content)}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{images.length > 0 && (
|
{images.length > 0 && (
|
||||||
|
|
@ -258,7 +259,7 @@ export default function CommentsPage({ user }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="comment-text">{c.content}</p>
|
<p className="comment-text">{decodeHtmlEntities(c.content)}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,31 @@ import { useNavigate } from 'react-router-dom'
|
||||||
import { Heart, MessageCircle, UserPlus, AtSign, CheckCheck } from 'lucide-react'
|
import { Heart, MessageCircle, UserPlus, AtSign, CheckCheck } from 'lucide-react'
|
||||||
import { getNotifications, markNotificationRead, markAllNotificationsRead } from '../utils/api'
|
import { getNotifications, markNotificationRead, markAllNotificationsRead } from '../utils/api'
|
||||||
import { hapticFeedback } from '../utils/telegram'
|
import { hapticFeedback } from '../utils/telegram'
|
||||||
|
import { decodeHtmlEntities } from '../utils/htmlEntities'
|
||||||
import './Notifications.css'
|
import './Notifications.css'
|
||||||
|
|
||||||
const NOTIFICATION_ICONS = {
|
const NOTIFICATION_ICONS = {
|
||||||
follow: UserPlus,
|
follow: UserPlus,
|
||||||
like: Heart,
|
like: Heart,
|
||||||
comment: MessageCircle,
|
comment: MessageCircle,
|
||||||
mention: AtSign
|
mention: AtSign,
|
||||||
|
new_post: Heart
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOTIFICATION_COLORS = {
|
const NOTIFICATION_COLORS = {
|
||||||
follow: '#007AFF',
|
follow: '#007AFF',
|
||||||
like: '#FF3B30',
|
like: '#FF3B30',
|
||||||
comment: '#34C759',
|
comment: '#34C759',
|
||||||
mention: '#FF9500'
|
mention: '#FF9500',
|
||||||
|
new_post: '#5856D6'
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOTIFICATION_TEXTS = {
|
const NOTIFICATION_TEXTS = {
|
||||||
follow: 'подписался на вас',
|
follow: 'подписался на вас',
|
||||||
like: 'лайкнул ваш пост',
|
like: 'лайкнул ваш пост',
|
||||||
comment: 'прокомментировал ваш пост',
|
comment: 'прокомментировал ваш пост',
|
||||||
mention: 'упомянул вас в посте'
|
mention: 'упомянул вас в посте',
|
||||||
|
new_post: 'опубликовал новый пост'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Notifications({ user }) {
|
export default function Notifications({ user }) {
|
||||||
|
|
@ -187,7 +191,7 @@ export default function Notifications({ user }) {
|
||||||
|
|
||||||
{notification.post && notification.post.content && (
|
{notification.post && notification.post.content && (
|
||||||
<div className="bubble-post-preview">
|
<div className="bubble-post-preview">
|
||||||
{notification.post.content.slice(0, 50)}
|
{decodeHtmlEntities(notification.post.content.slice(0, 50))}
|
||||||
{notification.post.content.length > 50 && '...'}
|
{notification.post.content.length > 50 && '...'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { ArrowLeft, Trash2, Flag, Edit, Share2 } from 'lucide-react'
|
import { ArrowLeft, Trash2, Flag, Edit, Share2 } from 'lucide-react'
|
||||||
import { getPosts, reportPost, deletePost, editPost } from '../utils/api'
|
import { getPosts, reportPost, deletePost, editPost } from '../utils/api'
|
||||||
import { hapticFeedback, showConfirm, showAlert } from '../utils/telegram'
|
import { hapticFeedback, showConfirm, showAlert } from '../utils/telegram'
|
||||||
|
import { decodeHtmlEntities } from '../utils/htmlEntities'
|
||||||
import './PostMenuPage.css'
|
import './PostMenuPage.css'
|
||||||
|
|
||||||
export default function PostMenuPage({ user }) {
|
export default function PostMenuPage({ user }) {
|
||||||
|
|
@ -203,7 +204,7 @@ export default function PostMenuPage({ user }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{post.content && (
|
{post.content && (
|
||||||
<div className="preview-content">{post.content}</div>
|
<div className="preview-content">{decodeHtmlEntities(post.content)}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(post.images && post.images.length > 0) || post.imageUrl ? (
|
{(post.images && post.images.length > 0) || post.imageUrl ? (
|
||||||
|
|
@ -221,11 +222,6 @@ export default function PostMenuPage({ user }) {
|
||||||
|
|
||||||
{/* Меню */}
|
{/* Меню */}
|
||||||
<div className="menu-items">
|
<div className="menu-items">
|
||||||
<button className="menu-item" onClick={handleShare}>
|
|
||||||
<Share2 size={20} />
|
|
||||||
<span>Поделиться</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{(post.author._id === user.id) || (user.role === 'moderator' || user.role === 'admin') ? (
|
{(post.author._id === user.id) || (user.role === 'moderator' || user.role === 'admin') ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ import { useState } from 'react'
|
||||||
import { Settings, Heart, Edit2, Shield, Copy, Users } from 'lucide-react'
|
import { Settings, Heart, Edit2, Shield, Copy, Users } from 'lucide-react'
|
||||||
import { updateProfile } from '../utils/api'
|
import { updateProfile } from '../utils/api'
|
||||||
import { hapticFeedback, showAlert } from '../utils/telegram'
|
import { hapticFeedback, showAlert } from '../utils/telegram'
|
||||||
|
import { decodeHtmlEntities } from '../utils/htmlEntities'
|
||||||
import ThemeToggle from '../components/ThemeToggle'
|
import ThemeToggle from '../components/ThemeToggle'
|
||||||
|
import FollowListModal from '../components/FollowListModal'
|
||||||
import './Profile.css'
|
import './Profile.css'
|
||||||
|
|
||||||
const DONATION_URL = 'https://donatepay.ru/don/1435720'
|
const DONATION_URL = 'https://donatepay.ru/don/1435720'
|
||||||
|
|
@ -38,6 +40,8 @@ export default function Profile({ user, setUser }) {
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
const [showSettings, setShowSettings] = useState(false)
|
||||||
const [showEditBio, setShowEditBio] = useState(false)
|
const [showEditBio, setShowEditBio] = useState(false)
|
||||||
const [bio, setBio] = useState(user.bio || '')
|
const [bio, setBio] = useState(user.bio || '')
|
||||||
|
const [showFollowers, setShowFollowers] = useState(false)
|
||||||
|
const [showFollowing, setShowFollowing] = useState(false)
|
||||||
const [settings, setSettings] = useState(normalizeSettings(user.settings))
|
const [settings, setSettings] = useState(normalizeSettings(user.settings))
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
|
|
@ -140,7 +144,7 @@ export default function Profile({ user, setUser }) {
|
||||||
|
|
||||||
{user.bio ? (
|
{user.bio ? (
|
||||||
<div className="profile-bio">
|
<div className="profile-bio">
|
||||||
<p>{user.bio}</p>
|
<p>{decodeHtmlEntities(user.bio)}</p>
|
||||||
<button className="edit-bio-btn" onClick={() => setShowEditBio(true)}>
|
<button className="edit-bio-btn" onClick={() => setShowEditBio(true)}>
|
||||||
<Edit2 size={16} />
|
<Edit2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -154,12 +158,12 @@ export default function Profile({ user, setUser }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="profile-stats">
|
<div className="profile-stats">
|
||||||
<div className="stat-item">
|
<div className="stat-item" onClick={() => setShowFollowers(true)} style={{ cursor: 'pointer' }}>
|
||||||
<span className="stat-value">{user.followersCount || 0}</span>
|
<span className="stat-value">{user.followersCount || 0}</span>
|
||||||
<span className="stat-label">Подписчики</span>
|
<span className="stat-label">Подписчики</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-divider" />
|
<div className="stat-divider" />
|
||||||
<div className="stat-item">
|
<div className="stat-item" onClick={() => setShowFollowing(true)} style={{ cursor: 'pointer' }}>
|
||||||
<span className="stat-value">{user.followingCount || 0}</span>
|
<span className="stat-value">{user.followingCount || 0}</span>
|
||||||
<span className="stat-label">Подписки</span>
|
<span className="stat-label">Подписки</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -390,6 +394,26 @@ export default function Profile({ user, setUser }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Модалка подписчиков */}
|
||||||
|
{showFollowers && (
|
||||||
|
<FollowListModal
|
||||||
|
users={user.followers || []}
|
||||||
|
title="Подписчики"
|
||||||
|
currentUser={user}
|
||||||
|
onClose={() => setShowFollowers(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Модалка подписок */}
|
||||||
|
{showFollowing && (
|
||||||
|
<FollowListModal
|
||||||
|
users={user.following || []}
|
||||||
|
title="Подписки"
|
||||||
|
currentUser={user}
|
||||||
|
onClose={() => setShowFollowing(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import { ChevronLeft, Shield } from 'lucide-react'
|
import { ChevronLeft, Shield } from 'lucide-react'
|
||||||
import { getUserProfile, getUserPosts, followUser } from '../utils/api'
|
import { getUserProfile, getUserPosts, followUser } from '../utils/api'
|
||||||
import { hapticFeedback } from '../utils/telegram'
|
import { hapticFeedback } from '../utils/telegram'
|
||||||
|
import { decodeHtmlEntities } from '../utils/htmlEntities'
|
||||||
import PostCard from '../components/PostCard'
|
import PostCard from '../components/PostCard'
|
||||||
|
import FollowListModal from '../components/FollowListModal'
|
||||||
import './UserProfile.css'
|
import './UserProfile.css'
|
||||||
|
|
||||||
export default function UserProfile({ currentUser }) {
|
export default function UserProfile({ currentUser }) {
|
||||||
|
|
@ -14,6 +16,8 @@ export default function UserProfile({ currentUser }) {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [following, setFollowing] = useState(false)
|
const [following, setFollowing] = useState(false)
|
||||||
const [followersCount, setFollowersCount] = useState(0)
|
const [followersCount, setFollowersCount] = useState(0)
|
||||||
|
const [showFollowers, setShowFollowers] = useState(false)
|
||||||
|
const [showFollowing, setShowFollowing] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProfile()
|
loadProfile()
|
||||||
|
|
@ -102,18 +106,18 @@ export default function UserProfile({ currentUser }) {
|
||||||
|
|
||||||
{user.bio && (
|
{user.bio && (
|
||||||
<div className="user-bio">
|
<div className="user-bio">
|
||||||
<p>{user.bio}</p>
|
<p>{decodeHtmlEntities(user.bio)}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="user-stats">
|
<div className="user-stats">
|
||||||
<div className="stat-item">
|
<div className="stat-item" onClick={() => setShowFollowers(true)} style={{ cursor: 'pointer' }}>
|
||||||
<span className="stat-value">{followersCount}</span>
|
<span className="stat-value">{followersCount}</span>
|
||||||
<span className="stat-label">Подписчики</span>
|
<span className="stat-label">Подписчики</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-divider" />
|
<div className="stat-divider" />
|
||||||
<div className="stat-item">
|
<div className="stat-item" onClick={() => setShowFollowing(true)} style={{ cursor: 'pointer' }}>
|
||||||
<span className="stat-value">{user.followingCount}</span>
|
<span className="stat-value">{user.followingCount}</span>
|
||||||
<span className="stat-label">Подписки</span>
|
<span className="stat-label">Подписки</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -152,6 +156,26 @@ export default function UserProfile({ currentUser }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Модалка подписчиков */}
|
||||||
|
{showFollowers && user && (
|
||||||
|
<FollowListModal
|
||||||
|
users={user.followers || []}
|
||||||
|
title="Подписчики"
|
||||||
|
currentUser={currentUser}
|
||||||
|
onClose={() => setShowFollowers(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Модалка подписок */}
|
||||||
|
{showFollowing && user && (
|
||||||
|
<FollowListModal
|
||||||
|
users={user.following || []}
|
||||||
|
title="Подписки"
|
||||||
|
currentUser={currentUser}
|
||||||
|
onClose={() => setShowFollowing(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Декодировать HTML entities (например, / -> /)
|
||||||
|
export function decodeHtmlEntities(str = '') {
|
||||||
|
if (!str || typeof str !== 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем временный элемент для декодирования
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.innerHTML = str;
|
||||||
|
return textarea.value;
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue