nakama/frontend/src/components/CreatePostModal.jsx

307 lines
10 KiB
React
Raw Normal View History

2025-11-03 20:35:01 +00:00
import { useState, useRef } from 'react'
2025-12-04 20:27:45 +00:00
import { createPortal } from 'react-dom'
2025-11-03 20:35:01 +00:00
import { X, Image as ImageIcon, Tag, AtSign } from 'lucide-react'
import { createPost, searchUsers } from '../utils/api'
import { hapticFeedback } from '../utils/telegram'
import './CreatePostModal.css'
const TAGS = [
{ value: 'furry', label: 'Furry', color: '#FF8A33' },
{ value: 'anime', label: 'Anime', color: '#4A90E2' },
{ value: 'other', label: 'Other', color: '#A0A0A0' }
]
2025-11-03 22:17:25 +00:00
export default function CreatePostModal({ user, onClose, onPostCreated, initialImage }) {
2025-11-03 20:35:01 +00:00
const [content, setContent] = useState('')
const [selectedTags, setSelectedTags] = useState([])
2025-11-03 22:17:25 +00:00
const [images, setImages] = useState(initialImage ? [initialImage] : [])
const [imagePreviews, setImagePreviews] = useState(initialImage ? [initialImage] : [])
const [externalImages, setExternalImages] = useState(initialImage ? [initialImage] : [])
2025-11-03 20:35:01 +00:00
const [isNSFW, setIsNSFW] = useState(false)
2025-12-01 14:26:18 +00:00
const [isHomo, setIsHomo] = useState(false)
2025-11-03 20:35:01 +00:00
const [loading, setLoading] = useState(false)
const [showUserSearch, setShowUserSearch] = useState(false)
const [userSearchQuery, setUserSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState([])
const [mentionedUsers, setMentionedUsers] = useState([])
const fileInputRef = useRef(null)
const handleImageSelect = (e) => {
2025-11-03 22:17:25 +00:00
const files = Array.from(e.target.files)
if (files.length === 0) return
// Максимум 5 изображений
const remainingSlots = 5 - images.length
const filesToAdd = files.slice(0, remainingSlots)
filesToAdd.forEach(file => {
2025-11-03 20:35:01 +00:00
const reader = new FileReader()
reader.onloadend = () => {
2025-11-03 22:17:25 +00:00
setImagePreviews(prev => [...prev, reader.result])
2025-11-03 20:35:01 +00:00
}
reader.readAsDataURL(file)
2025-11-03 22:17:25 +00:00
})
setImages(prev => [...prev, ...filesToAdd])
hapticFeedback('light')
2025-11-03 20:35:01 +00:00
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
2025-11-03 22:17:25 +00:00
const handleRemoveImage = (index) => {
setImages(prev => prev.filter((_, i) => i !== index))
setImagePreviews(prev => prev.filter((_, i) => i !== index))
setExternalImages(prev => prev.filter((_, i) => i !== index))
}
2025-11-03 20:35:01 +00:00
const toggleTag = (tag) => {
hapticFeedback('light')
if (selectedTags.includes(tag)) {
setSelectedTags(selectedTags.filter(t => t !== tag))
} else {
setSelectedTags([...selectedTags, tag])
}
}
const handleUserSearch = async (query) => {
setUserSearchQuery(query)
if (query.length > 1) {
try {
const users = await searchUsers(query)
setSearchResults(users)
} catch (error) {
console.error('Ошибка поиска:', error)
}
} else {
setSearchResults([])
}
}
const handleMentionUser = (user) => {
if (!mentionedUsers.find(u => u._id === user._id)) {
setMentionedUsers([...mentionedUsers, user])
setContent(prev => prev + `@${user.username} `)
}
setShowUserSearch(false)
setUserSearchQuery('')
setSearchResults([])
hapticFeedback('light')
}
const handleSubmit = async () => {
if (selectedTags.length === 0) {
alert('Выберите хотя бы один тег')
return
}
2025-11-03 22:17:25 +00:00
if (!content.trim() && images.length === 0) {
2025-11-03 20:35:01 +00:00
alert('Добавьте текст или изображение')
return
}
try {
setLoading(true)
hapticFeedback('light')
const formData = new FormData()
formData.append('content', content)
formData.append('tags', JSON.stringify(selectedTags))
formData.append('isNSFW', isNSFW)
2025-12-01 14:26:18 +00:00
formData.append('isHomo', isHomo)
2025-11-03 20:35:01 +00:00
2025-11-03 22:17:25 +00:00
// Добавить загруженные файлы
images.forEach((image, index) => {
if (image instanceof File) {
formData.append('images', image)
}
})
// Добавить внешние изображения (из поиска)
if (externalImages.length > 0) {
formData.append('externalImages', JSON.stringify(externalImages))
2025-11-03 20:35:01 +00:00
}
if (mentionedUsers.length > 0) {
formData.append('mentionedUsers', JSON.stringify(mentionedUsers.map(u => u._id)))
}
const newPost = await createPost(formData)
hapticFeedback('success')
onPostCreated(newPost)
} catch (error) {
console.error('Ошибка создания поста:', error)
hapticFeedback('error')
alert('Ошибка создания поста')
} finally {
setLoading(false)
}
}
2025-12-04 20:27:45 +00:00
return createPortal(
<div
className="modal-overlay"
onMouseDown={(e) => e.stopPropagation()}
onTouchStart={(e) => e.stopPropagation()}
onClick={onClose}
>
2025-11-03 20:35:01 +00:00
<div className="modal-content create-post-modal" onClick={e => e.stopPropagation()}>
{/* Хедер */}
<div className="modal-header">
<button className="close-btn" onClick={onClose}>
<X size={24} />
</button>
<h2>Создать пост</h2>
<button
className="submit-btn"
onClick={handleSubmit}
disabled={loading || selectedTags.length === 0}
>
{loading ? 'Отправка...' : 'Опубликовать'}
</button>
</div>
{/* Контент */}
<div className="modal-body">
<textarea
placeholder="Что нового?"
value={content}
onChange={e => setContent(e.target.value)}
maxLength={2000}
rows={6}
/>
2025-11-03 22:17:25 +00:00
{/* Превью изображений */}
{imagePreviews.length > 0 && (
<div className="images-preview">
{imagePreviews.map((preview, index) => (
<div key={index} className="image-preview">
<img src={preview} alt={`Preview ${index + 1}`} />
<button className="remove-image-btn" onClick={() => handleRemoveImage(index)}>
<X size={16} />
</button>
</div>
))}
2025-11-03 20:35:01 +00:00
</div>
)}
{/* Выбор тегов */}
<div className="tags-section">
<div className="section-label">
<Tag size={18} />
<span>Теги (обязательно)</span>
</div>
<div className="tags-list">
{TAGS.map(tag => (
<button
key={tag.value}
className={`tag-btn ${selectedTags.includes(tag.value) ? 'active' : ''}`}
style={{
backgroundColor: selectedTags.includes(tag.value) ? tag.color : 'var(--bg-primary)',
color: selectedTags.includes(tag.value) ? 'white' : 'var(--text-primary)'
}}
onClick={() => toggleTag(tag.value)}
>
{tag.label}
</button>
))}
</div>
</div>
{/* Упомянутые пользователи */}
{mentionedUsers.length > 0 && (
<div className="mentioned-users">
{mentionedUsers.map(u => (
<span key={u._id} className="mentioned-user">
@{u.username}
</span>
))}
</div>
)}
{/* NSFW переключатель */}
<div className="nsfw-toggle">
<label>
<input
type="checkbox"
checked={isNSFW}
onChange={e => setIsNSFW(e.target.checked)}
/>
<span>Отметить как NSFW</span>
</label>
</div>
2025-12-01 14:26:18 +00:00
{/* Homo переключатель */}
<div className="nsfw-toggle">
<label>
<input
type="checkbox"
checked={isHomo}
onChange={e => setIsHomo(e.target.checked)}
/>
<span>Отметить как Homo</span>
</label>
</div>
2025-11-03 20:35:01 +00:00
</div>
{/* Футер с действиями */}
<div className="modal-footer">
<input
ref={fileInputRef}
type="file"
accept="image/*"
2025-11-03 22:17:25 +00:00
multiple
2025-11-03 20:35:01 +00:00
onChange={handleImageSelect}
style={{ display: 'none' }}
/>
2025-11-03 22:17:25 +00:00
<button
className="action-icon-btn"
onClick={() => fileInputRef.current?.click()}
disabled={images.length >= 5}
>
2025-11-03 20:35:01 +00:00
<ImageIcon size={22} />
2025-11-03 22:17:25 +00:00
{images.length > 0 && <span className="image-count">{images.length}/5</span>}
2025-11-03 20:35:01 +00:00
</button>
<button className="action-icon-btn" onClick={() => setShowUserSearch(true)}>
<AtSign size={22} />
</button>
</div>
{/* Поиск пользователей */}
{showUserSearch && (
<div className="user-search-modal">
<div className="user-search-header">
<input
type="text"
placeholder="Поиск пользователей..."
value={userSearchQuery}
onChange={e => handleUserSearch(e.target.value)}
autoFocus
/>
<button onClick={() => setShowUserSearch(false)}>
<X size={20} />
</button>
</div>
<div className="user-search-results">
{searchResults.map(u => (
<div key={u._id} className="user-result" onClick={() => handleMentionUser(u)}>
<img src={u.photoUrl || '/default-avatar.png'} alt={u.username} />
<div>
<div className="user-name">{u.firstName} {u.lastName}</div>
<div className="user-username">@{u.username}</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
2025-12-04 20:27:45 +00:00
</div>,
document.body
2025-11-03 20:35:01 +00:00
)
}