From 66149df2a58dd0e7a801c4aae123ae42d831b64b Mon Sep 17 00:00:00 2001 From: glpshchn <464976@niuitmo.ru> Date: Thu, 4 Dec 2025 23:00:39 +0300 Subject: [PATCH] Update files --- backend/models/Notification.js | 2 +- backend/routes/posts.js | 13 ++ backend/routes/users.js | 2 + frontend/src/components/CommentsModal.jsx | 9 +- frontend/src/components/FollowListModal.css | 178 ++++++++++++++++++++ frontend/src/components/FollowListModal.jsx | 131 ++++++++++++++ frontend/src/components/PostCard.css | 14 +- frontend/src/components/PostCard.jsx | 54 +++--- frontend/src/pages/CommentsPage.jsx | 5 +- frontend/src/pages/Notifications.jsx | 12 +- frontend/src/pages/PostMenuPage.jsx | 8 +- frontend/src/pages/Profile.jsx | 30 +++- frontend/src/pages/UserProfile.jsx | 30 +++- frontend/src/utils/htmlEntities.js | 12 ++ 14 files changed, 447 insertions(+), 53 deletions(-) create mode 100644 frontend/src/components/FollowListModal.css create mode 100644 frontend/src/components/FollowListModal.jsx create mode 100644 frontend/src/utils/htmlEntities.js diff --git a/backend/models/Notification.js b/backend/models/Notification.js index dc94f97..d9676a4 100644 --- a/backend/models/Notification.js +++ b/backend/models/Notification.js @@ -13,7 +13,7 @@ const NotificationSchema = new mongoose.Schema({ }, type: { type: String, - enum: ['follow', 'like', 'comment', 'mention'], + enum: ['follow', 'like', 'comment', 'mention', 'new_post'], required: true }, post: { diff --git a/backend/routes/posts.js b/backend/routes/posts.js index e5d9be7..16abb0b 100644 --- a/backend/routes/posts.js +++ b/backend/routes/posts.js @@ -174,6 +174,19 @@ router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploa })); 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 }); } catch (error) { diff --git a/backend/routes/users.js b/backend/routes/users.js index 35b8e3b..6e19187 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -32,6 +32,8 @@ router.get('/:id', authenticate, async (req, res) => { bio: user.bio, followersCount: user.followers.length, followingCount: user.following.length, + followers: user.followers, + following: user.following, isFollowing, createdAt: user.createdAt } diff --git a/frontend/src/components/CommentsModal.jsx b/frontend/src/components/CommentsModal.jsx index fb8846f..d01be52 100644 --- a/frontend/src/components/CommentsModal.jsx +++ b/frontend/src/components/CommentsModal.jsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { X, Send } from 'lucide-react' import { commentPost } from '../utils/api' import { hapticFeedback } from '../utils/telegram' +import { decodeHtmlEntities } from '../utils/htmlEntities' import './CommentsModal.css' export default function CommentsModal({ post, onClose, onUpdate }) { @@ -79,12 +80,12 @@ export default function CommentsModal({ post, onClose, onUpdate }) { {post.content && ( -
{c.content}
+{decodeHtmlEntities(c.content)}
)) diff --git a/frontend/src/components/FollowListModal.css b/frontend/src/components/FollowListModal.css new file mode 100644 index 0000000..21fce4d --- /dev/null +++ b/frontend/src/components/FollowListModal.css @@ -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; +} + + diff --git a/frontend/src/components/FollowListModal.jsx b/frontend/src/components/FollowListModal.jsx new file mode 100644 index 0000000..824b691 --- /dev/null +++ b/frontend/src/components/FollowListModal.jsx @@ -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 ( +Пока никого нет
+{c.content}
+{decodeHtmlEntities(c.content)}
)} diff --git a/frontend/src/pages/Notifications.jsx b/frontend/src/pages/Notifications.jsx index a3b8de0..7cac6df 100644 --- a/frontend/src/pages/Notifications.jsx +++ b/frontend/src/pages/Notifications.jsx @@ -3,27 +3,31 @@ import { useNavigate } from 'react-router-dom' import { Heart, MessageCircle, UserPlus, AtSign, CheckCheck } from 'lucide-react' import { getNotifications, markNotificationRead, markAllNotificationsRead } from '../utils/api' import { hapticFeedback } from '../utils/telegram' +import { decodeHtmlEntities } from '../utils/htmlEntities' import './Notifications.css' const NOTIFICATION_ICONS = { follow: UserPlus, like: Heart, comment: MessageCircle, - mention: AtSign + mention: AtSign, + new_post: Heart } const NOTIFICATION_COLORS = { follow: '#007AFF', like: '#FF3B30', comment: '#34C759', - mention: '#FF9500' + mention: '#FF9500', + new_post: '#5856D6' } const NOTIFICATION_TEXTS = { follow: 'подписался на вас', like: 'лайкнул ваш пост', comment: 'прокомментировал ваш пост', - mention: 'упомянул вас в посте' + mention: 'упомянул вас в посте', + new_post: 'опубликовал новый пост' } export default function Notifications({ user }) { @@ -187,7 +191,7 @@ export default function Notifications({ user }) { {notification.post && notification.post.content && ({user.bio}
+{decodeHtmlEntities(user.bio)}