2025-11-03 20:35:01 +00:00
|
|
|
|
import { useState, useRef } from 'react'
|
|
|
|
|
|
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)
|
|
|
|
|
|
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-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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="modal-overlay" onClick={onClose}>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</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>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|