307 lines
10 KiB
JavaScript
307 lines
10 KiB
JavaScript
import { useState, useRef } from 'react'
|
||
import { createPortal } from 'react-dom'
|
||
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' }
|
||
]
|
||
|
||
export default function CreatePostModal({ user, onClose, onPostCreated, initialImage }) {
|
||
const [content, setContent] = useState('')
|
||
const [selectedTags, setSelectedTags] = useState([])
|
||
const [images, setImages] = useState(initialImage ? [initialImage] : [])
|
||
const [imagePreviews, setImagePreviews] = useState(initialImage ? [initialImage] : [])
|
||
const [externalImages, setExternalImages] = useState(initialImage ? [initialImage] : [])
|
||
const [isNSFW, setIsNSFW] = useState(false)
|
||
const [isHomo, setIsHomo] = 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) => {
|
||
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 => {
|
||
const reader = new FileReader()
|
||
reader.onloadend = () => {
|
||
setImagePreviews(prev => [...prev, reader.result])
|
||
}
|
||
reader.readAsDataURL(file)
|
||
})
|
||
|
||
setImages(prev => [...prev, ...filesToAdd])
|
||
hapticFeedback('light')
|
||
|
||
if (fileInputRef.current) {
|
||
fileInputRef.current.value = ''
|
||
}
|
||
}
|
||
|
||
const handleRemoveImage = (index) => {
|
||
setImages(prev => prev.filter((_, i) => i !== index))
|
||
setImagePreviews(prev => prev.filter((_, i) => i !== index))
|
||
setExternalImages(prev => prev.filter((_, i) => i !== index))
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
if (!content.trim() && images.length === 0) {
|
||
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)
|
||
formData.append('isHomo', isHomo)
|
||
|
||
// Добавить загруженные файлы
|
||
images.forEach((image, index) => {
|
||
if (image instanceof File) {
|
||
formData.append('images', image)
|
||
}
|
||
})
|
||
|
||
// Добавить внешние изображения (из поиска)
|
||
if (externalImages.length > 0) {
|
||
formData.append('externalImages', JSON.stringify(externalImages))
|
||
}
|
||
|
||
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 createPortal(
|
||
<div
|
||
className="modal-overlay"
|
||
onMouseDown={(e) => e.stopPropagation()}
|
||
onTouchStart={(e) => e.stopPropagation()}
|
||
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}
|
||
/>
|
||
|
||
{/* Превью изображений */}
|
||
{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>
|
||
))}
|
||
</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>
|
||
|
||
{/* Homo переключатель */}
|
||
<div className="nsfw-toggle">
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
checked={isHomo}
|
||
onChange={e => setIsHomo(e.target.checked)}
|
||
/>
|
||
<span>Отметить как Homo</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Футер с действиями */}
|
||
<div className="modal-footer">
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
onChange={handleImageSelect}
|
||
style={{ display: 'none' }}
|
||
/>
|
||
|
||
<button
|
||
className="action-icon-btn"
|
||
onClick={() => fileInputRef.current?.click()}
|
||
disabled={images.length >= 5}
|
||
>
|
||
<ImageIcon size={22} />
|
||
{images.length > 0 && <span className="image-count">{images.length}/5</span>}
|
||
</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>,
|
||
document.body
|
||
)
|
||
}
|
||
|