nakama/frontend/src/components/CreatePostModal.jsx

307 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
)
}