Добавлены новые зависимости для работы с эмодзи и улучшена структура базы данных. Реализована модель сообщений и обновлены компоненты для поддержки новых функций мессенджера. Обновлены запросы и мутации для работы с сообщениями и чатом.

This commit is contained in:
Bivekich
2025-07-16 22:07:38 +03:00
parent 823ef9a28c
commit 205c9eae98
33 changed files with 3288 additions and 229 deletions

View File

@ -0,0 +1,68 @@
"use client"
import dynamic from 'next/dynamic'
import { useState, useRef, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Smile } from 'lucide-react'
// Динамически импортируем EmojiPicker чтобы избежать проблем с SSR
const EmojiPicker = dynamic(
() => import('emoji-picker-react'),
{ ssr: false }
)
interface EmojiPickerComponentProps {
onEmojiSelect: (emoji: string) => void
}
export function EmojiPickerComponent({ onEmojiSelect }: EmojiPickerComponentProps) {
const [showPicker, setShowPicker] = useState(false)
const pickerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (pickerRef.current && !pickerRef.current.contains(event.target as Node)) {
setShowPicker(false)
}
}
if (showPicker) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [showPicker])
const handleEmojiClick = (emojiData: { emoji: string }) => {
onEmojiSelect(emojiData.emoji)
setShowPicker(false)
}
return (
<div className="relative" ref={pickerRef}>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowPicker(!showPicker)}
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
>
<Smile className="h-4 w-4" />
</Button>
{showPicker && (
<div className="absolute bottom-full right-0 mb-2 z-50">
<div className="bg-gray-800 border border-white/20 rounded-lg overflow-hidden shadow-xl">
<EmojiPicker
onEmojiClick={handleEmojiClick}
width={350}
height={400}
/>
</div>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,108 @@
"use client"
import { Download, FileText, Archive, Image as ImageIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
interface FileMessageProps {
fileUrl: string
fileName: string
fileSize?: number
fileType?: string
isCurrentUser?: boolean
}
export function FileMessage({ fileUrl, fileName, fileSize, fileType, isCurrentUser = false }: FileMessageProps) {
const formatFileSize = (bytes?: number) => {
if (!bytes) return ''
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
const getFileIcon = (type?: string) => {
if (!type) return <FileText className="h-6 w-6" />
if (type.includes('pdf')) return <FileText className="h-6 w-6 text-red-400" />
if (type.includes('zip') || type.includes('archive')) return <Archive className="h-6 w-6 text-yellow-400" />
if (type.includes('image')) return <ImageIcon className="h-6 w-6 text-green-400" />
if (type.includes('word') || type.includes('document')) return <FileText className="h-6 w-6 text-blue-400" />
if (type.includes('excel') || type.includes('spreadsheet')) return <FileText className="h-6 w-6 text-green-600" />
if (type.includes('powerpoint') || type.includes('presentation')) return <FileText className="h-6 w-6 text-orange-400" />
return <FileText className="h-6 w-6" />
}
const getFileExtension = (name: string) => {
const parts = name.split('.')
return parts.length > 1 ? parts.pop()?.toUpperCase() : 'FILE'
}
const handleDownload = () => {
const link = document.createElement('a')
link.href = fileUrl
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
return (
<div className={`flex items-center space-x-3 p-3 rounded-lg max-w-sm cursor-pointer hover:opacity-80 transition-opacity ${
isCurrentUser
? 'bg-blue-500/20 border border-blue-500/30'
: 'bg-white/10 border border-white/20'
}`} onClick={handleDownload}>
{/* Иконка файла */}
<div className={`flex-shrink-0 w-12 h-12 rounded-lg flex items-center justify-center ${
isCurrentUser ? 'bg-blue-500/30' : 'bg-white/20'
}`}>
{getFileIcon(fileType)}
</div>
{/* Информация о файле */}
<div className="flex-1 min-w-0">
<p className={`font-medium text-sm truncate ${
isCurrentUser ? 'text-white' : 'text-white'
}`}>
{fileName}
</p>
<div className="flex items-center space-x-2 mt-1">
{fileSize && (
<span className={`text-xs ${
isCurrentUser ? 'text-blue-300/70' : 'text-white/60'
}`}>
{formatFileSize(fileSize)}
</span>
)}
<span className={`text-xs ${
isCurrentUser ? 'text-blue-300/70' : 'text-white/60'
}`}>
{getFileExtension(fileName)}
</span>
</div>
</div>
{/* Кнопка скачивания */}
<Button
onClick={(e) => {
e.stopPropagation()
handleDownload()
}}
size="sm"
variant="ghost"
className={`flex-shrink-0 ${
isCurrentUser
? 'text-blue-300 hover:text-blue-200 hover:bg-blue-500/30'
: 'text-white/60 hover:text-white hover:bg-white/20'
}`}
>
<Download className="h-4 w-4" />
</Button>
</div>
)
}

View File

@ -0,0 +1,216 @@
"use client"
import { useState, useRef } from 'react'
import { Button } from '@/components/ui/button'
import { Paperclip, Image, X } from 'lucide-react'
import { useAuth } from '@/hooks/useAuth'
interface FileUploaderProps {
onSendFile: (fileUrl: string, fileName: string, fileSize: number, fileType: string, messageType: 'IMAGE' | 'FILE') => void
}
interface UploadedFile {
url: string
name: string
size: number
type: string
messageType: 'IMAGE' | 'FILE'
}
export function FileUploader({ onSendFile }: FileUploaderProps) {
const { user } = useAuth()
const [isUploading, setIsUploading] = useState(false)
const [selectedFile, setSelectedFile] = useState<UploadedFile | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const imageInputRef = useRef<HTMLInputElement>(null)
const isImageType = (type: string) => {
return type.startsWith('image/')
}
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const handleFileSelect = async (file: File, messageType: 'IMAGE' | 'FILE') => {
if (!user?.id) return
setIsUploading(true)
try {
const formData = new FormData()
formData.append('file', file)
formData.append('userId', user.id)
formData.append('messageType', messageType)
const response = await fetch('/api/upload-file', {
method: 'POST',
body: formData
})
if (!response.ok) {
let errorMessage = 'Failed to upload file'
try {
const errorData = await response.json()
errorMessage = errorData.error || errorMessage
} catch {
errorMessage = `HTTP ${response.status}: ${response.statusText}`
}
throw new Error(errorMessage)
}
const result = await response.json()
if (!result.success) {
throw new Error(result.error || 'Upload failed')
}
setSelectedFile({
url: result.url,
name: result.originalName,
size: result.size,
type: result.type,
messageType: result.messageType
})
} catch (error) {
console.error('Error uploading file:', error)
const errorMessage = error instanceof Error ? error.message : 'Неизвестная ошибка'
alert(`Ошибка при загрузке файла: ${errorMessage}`)
} finally {
setIsUploading(false)
}
}
const handleFileInputChange = (event: React.ChangeEvent<HTMLInputElement>, messageType: 'IMAGE' | 'FILE') => {
const file = event.target.files?.[0]
if (file) {
handleFileSelect(file, messageType)
}
// Очищаем input для возможности выбора того же файла
event.target.value = ''
}
const handleSendFile = () => {
if (selectedFile) {
onSendFile(
selectedFile.url,
selectedFile.name,
selectedFile.size,
selectedFile.type,
selectedFile.messageType
)
setSelectedFile(null)
}
}
const handleCancelFile = () => {
setSelectedFile(null)
}
if (selectedFile) {
return (
<div className="flex items-center space-x-2 bg-white/10 rounded-lg p-3 border border-white/20">
<div className="flex items-center space-x-2 flex-1">
{isImageType(selectedFile.type) ? (
<div className="flex items-center space-x-2">
<img
src={selectedFile.url}
alt="Preview"
className="w-10 h-10 object-cover rounded"
/>
<div>
<p className="text-white text-sm font-medium">{selectedFile.name}</p>
<p className="text-white/60 text-xs">{formatFileSize(selectedFile.size)}</p>
</div>
</div>
) : (
<div className="flex items-center space-x-2">
<div className="w-10 h-10 bg-blue-500/20 rounded flex items-center justify-center">
<Paperclip className="h-5 w-5 text-blue-300" />
</div>
<div>
<p className="text-white text-sm font-medium">{selectedFile.name}</p>
<p className="text-white/60 text-xs">{formatFileSize(selectedFile.size)}</p>
</div>
</div>
)}
</div>
<div className="flex items-center space-x-1">
<Button
onClick={handleSendFile}
size="sm"
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border-green-500/30"
variant="outline"
>
Отправить
</Button>
<Button
onClick={handleCancelFile}
size="sm"
variant="ghost"
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
)
}
return (
<div className="flex items-center space-x-2">
{/* Кнопка для загрузки изображений */}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => imageInputRef.current?.click()}
disabled={isUploading}
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
>
<Image className="h-4 w-4" />
</Button>
{/* Кнопка для загрузки файлов */}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
>
<Paperclip className="h-4 w-4" />
</Button>
{/* Скрытые input элементы */}
<input
ref={imageInputRef}
type="file"
accept="image/*"
onChange={(e) => handleFileInputChange(e, 'IMAGE')}
className="hidden"
/>
<input
ref={fileInputRef}
type="file"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.json"
onChange={(e) => handleFileInputChange(e, 'FILE')}
className="hidden"
/>
{isUploading && (
<div className="text-white/60 text-xs">
Загрузка...
</div>
)}
</div>
)
}

View File

@ -0,0 +1,129 @@
"use client"
import { useState } from 'react'
import { Download, Eye } from 'lucide-react'
import { Button } from '@/components/ui/button'
interface ImageMessageProps {
imageUrl: string
fileName: string
fileSize?: number
isCurrentUser?: boolean
}
export function ImageMessage({ imageUrl, fileName, fileSize, isCurrentUser = false }: ImageMessageProps) {
const [isLoading, setIsLoading] = useState(true)
const [showFullSize, setShowFullSize] = useState(false)
const formatFileSize = (bytes?: number) => {
if (!bytes) return ''
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
const handleDownload = () => {
const link = document.createElement('a')
link.href = imageUrl
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const handleImageClick = () => {
setShowFullSize(true)
}
return (
<>
<div className={`relative group max-w-xs ${
isCurrentUser
? 'bg-blue-500/20 border border-blue-500/30'
: 'bg-white/10 border border-white/20'
} rounded-lg overflow-hidden`}>
<div className="relative">
<img
src={imageUrl}
alt={fileName}
className="w-full h-auto cursor-pointer transition-opacity duration-200"
style={{
opacity: isLoading ? 0 : 1,
maxHeight: '300px',
objectFit: 'cover'
}}
onLoad={() => setIsLoading(false)}
onClick={handleImageClick}
/>
{isLoading && (
<div className="absolute inset-0 bg-gray-700 animate-pulse flex items-center justify-center">
<div className="text-white/60 text-sm">Загрузка...</div>
</div>
)}
{/* Overlay с кнопками при наведении */}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center space-x-2">
<Button
onClick={handleImageClick}
size="sm"
variant="ghost"
className="text-white hover:bg-white/20"
>
<Eye className="h-4 w-4" />
</Button>
<Button
onClick={handleDownload}
size="sm"
variant="ghost"
className="text-white hover:bg-white/20"
>
<Download className="h-4 w-4" />
</Button>
</div>
</div>
{/* Информация о файле */}
<div className="p-2">
<p className={`text-xs ${
isCurrentUser ? 'text-blue-300/70' : 'text-white/60'
} truncate`}>
{fileName}
</p>
{fileSize && (
<p className={`text-xs ${
isCurrentUser ? 'text-blue-300/50' : 'text-white/40'
}`}>
{formatFileSize(fileSize)}
</p>
)}
</div>
</div>
{/* Полноэкранный просмотр */}
{showFullSize && (
<div
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
onClick={() => setShowFullSize(false)}
>
<div className="relative max-w-full max-h-full">
<img
src={imageUrl}
alt={fileName}
className="max-w-full max-h-full object-contain"
onClick={(e) => e.stopPropagation()}
/>
<Button
onClick={() => setShowFullSize(false)}
className="absolute top-4 right-4 bg-black/50 hover:bg-black/70 text-white"
size="sm"
>
×
</Button>
</div>
</div>
)}
</>
)
}

View File

@ -0,0 +1,174 @@
"use client"
import { useState, useRef, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Play, Pause, Volume2 } from 'lucide-react'
interface VoicePlayerProps {
audioUrl: string
duration?: number
isCurrentUser?: boolean
}
export function VoicePlayer({ audioUrl, duration = 0, isCurrentUser = false }: VoicePlayerProps) {
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [audioDuration, setAudioDuration] = useState(duration > 0 ? duration : 0)
const [isLoading, setIsLoading] = useState(false)
const audioRef = useRef<HTMLAudioElement | null>(null)
// Обновляем длительность при изменении props
useEffect(() => {
if (duration > 0 && (!audioDuration || audioDuration === 0)) {
setAudioDuration(duration)
}
}, [duration, audioDuration])
useEffect(() => {
// Создаем аудио элемент
audioRef.current = new Audio(audioUrl)
const audio = audioRef.current
const handleLoadedMetadata = () => {
if (audio.duration && isFinite(audio.duration) && !isNaN(audio.duration)) {
setAudioDuration(audio.duration)
} else {
setAudioDuration(duration || 0)
}
setIsLoading(false)
}
const handleTimeUpdate = () => {
if (audio.currentTime && isFinite(audio.currentTime) && !isNaN(audio.currentTime)) {
setCurrentTime(audio.currentTime)
}
}
const handleEnded = () => {
setIsPlaying(false)
setCurrentTime(0)
}
const handleCanPlay = () => {
setIsLoading(false)
}
const handleLoadStart = () => {
setIsLoading(true)
}
const handleError = () => {
console.error('Audio loading error')
setIsLoading(false)
setIsPlaying(false)
}
audio.addEventListener('loadedmetadata', handleLoadedMetadata)
audio.addEventListener('timeupdate', handleTimeUpdate)
audio.addEventListener('ended', handleEnded)
audio.addEventListener('canplay', handleCanPlay)
audio.addEventListener('loadstart', handleLoadStart)
audio.addEventListener('error', handleError)
return () => {
if (audio) {
audio.removeEventListener('loadedmetadata', handleLoadedMetadata)
audio.removeEventListener('timeupdate', handleTimeUpdate)
audio.removeEventListener('ended', handleEnded)
audio.removeEventListener('canplay', handleCanPlay)
audio.removeEventListener('loadstart', handleLoadStart)
audio.removeEventListener('error', handleError)
audio.pause()
}
}
}, [audioUrl])
const togglePlayPause = () => {
const audio = audioRef.current
if (!audio) return
if (isPlaying) {
audio.pause()
setIsPlaying(false)
} else {
audio.play().then(() => {
setIsPlaying(true)
}).catch((error) => {
console.error('Error playing audio:', error)
setIsPlaying(false)
})
}
}
const formatTime = (time: number) => {
if (isNaN(time) || !isFinite(time) || time < 0) return '0:00'
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
const getProgress = () => {
if (!audioDuration || audioDuration === 0 || isNaN(audioDuration) || !isFinite(audioDuration)) return 0
if (!currentTime || isNaN(currentTime) || !isFinite(currentTime)) return 0
return Math.min((currentTime / audioDuration) * 100, 100)
}
return (
<div className={`flex items-center space-x-4 p-3 rounded-lg min-w-[200px] max-w-sm ${
isCurrentUser
? 'bg-blue-500/20 border border-blue-500/30'
: 'bg-white/10 border border-white/20'
}`}>
{/* Кнопка воспроизведения */}
<Button
onClick={togglePlayPause}
disabled={isLoading}
variant="ghost"
size="sm"
className={`p-2 rounded-full ${
isCurrentUser
? 'text-blue-300 hover:text-blue-200 hover:bg-blue-500/30'
: 'text-white hover:text-white/80 hover:bg-white/20'
}`}
>
{isLoading ? (
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
) : isPlaying ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
</Button>
{/* Визуализация волны / прогресс бар */}
<div className="flex-1 space-y-2">
<div className="flex items-center space-x-3">
<Volume2 className={`h-3 w-3 ${
isCurrentUser ? 'text-blue-300' : 'text-white/60'
}`} />
<div className="flex-1 h-1 bg-white/20 rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-300 ${
isCurrentUser ? 'bg-blue-400' : 'bg-white/60'
}`}
style={{ width: `${getProgress()}%` }}
/>
</div>
</div>
{/* Время */}
<div className="flex justify-between items-center text-xs">
<span className={`${isCurrentUser ? 'text-blue-300/70' : 'text-white/50'} min-w-[2rem]`}>
{formatTime(currentTime)}
</span>
<span className={`${isCurrentUser ? 'text-blue-300/70' : 'text-white/50'} min-w-[2rem] text-right`}>
{formatTime(audioDuration)}
</span>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,254 @@
"use client"
import { useState, useRef, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Mic, MicOff, Square, Send, Trash2 } from 'lucide-react'
import { useAuth } from '@/hooks/useAuth'
interface VoiceRecorderProps {
onSendVoice: (audioUrl: string, duration: number) => void
}
export function VoiceRecorder({ onSendVoice }: VoiceRecorderProps) {
const { user } = useAuth()
const [isRecording, setIsRecording] = useState(false)
const [recordedAudio, setRecordedAudio] = useState<string | null>(null)
const [duration, setDuration] = useState(0)
const [isPlaying, setIsPlaying] = useState(false)
const [permission, setPermission] = useState<'granted' | 'denied' | 'prompt'>('prompt')
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const audioRef = useRef<HTMLAudioElement | null>(null)
const intervalRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
// Проверяем доступность микрофона
if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
navigator.permissions.query({ name: 'microphone' as PermissionName }).then((result) => {
setPermission(result.state as 'granted' | 'denied' | 'prompt')
}).catch(() => {
// Если permissions API недоступен, оставляем prompt
})
}
}, [])
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
setPermission('granted')
const mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
})
mediaRecorderRef.current = mediaRecorder
audioChunksRef.current = []
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' })
const audioUrl = URL.createObjectURL(audioBlob)
setRecordedAudio(audioUrl)
// Останавливаем все треки для освобождения микрофона
stream.getTracks().forEach(track => track.stop())
// Останавливаем таймер
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
mediaRecorder.start()
setIsRecording(true)
setDuration(0)
// Запускаем таймер записи
intervalRef.current = setInterval(() => {
setDuration(prev => prev + 1)
}, 1000)
} catch (error) {
console.error('Error accessing microphone:', error)
setPermission('denied')
}
}
const stopRecording = () => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop()
setIsRecording(false)
}
}
const cancelRecording = () => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop()
setIsRecording(false)
}
setRecordedAudio(null)
setDuration(0)
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
const playRecording = () => {
if (recordedAudio) {
if (audioRef.current) {
audioRef.current.pause()
audioRef.current.currentTime = 0
}
audioRef.current = new Audio(recordedAudio)
audioRef.current.play()
setIsPlaying(true)
audioRef.current.onended = () => {
setIsPlaying(false)
}
}
}
const sendVoiceMessage = async () => {
if (!recordedAudio || !user?.id) return
try {
// Конвертируем Blob в File для загрузки
const response = await fetch(recordedAudio)
const blob = await response.blob()
const file = new File([blob], `voice-${Date.now()}.webm`, { type: 'audio/webm' })
// Загружаем в S3
const formData = new FormData()
formData.append('file', file)
formData.append('userId', user.id)
const uploadResponse = await fetch('/api/upload-voice', {
method: 'POST',
body: formData
})
if (!uploadResponse.ok) {
throw new Error('Failed to upload voice message')
}
const result = await uploadResponse.json()
// Отправляем голосовое сообщение
onSendVoice(result.url, duration)
// Очищаем состояние
setRecordedAudio(null)
setDuration(0)
} catch (error) {
console.error('Error sending voice message:', error)
}
}
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}
if (permission === 'denied') {
return (
<div className="text-white/60 text-xs text-center p-2">
Доступ к микрофону запрещен
</div>
)
}
return (
<div className="flex items-center space-x-2">
{!recordedAudio ? (
// Состояние записи
<>
{!isRecording ? (
<Button
type="button"
variant="ghost"
size="sm"
onClick={startRecording}
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
>
<Mic className="h-4 w-4" />
</Button>
) : (
<>
<div className="flex items-center space-x-2 bg-red-500/20 rounded-lg px-3 py-2">
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse" />
<span className="text-white text-sm font-mono">
{formatDuration(duration)}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={stopRecording}
className="text-white/80 hover:text-white p-1"
>
<Square className="h-3 w-3" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={cancelRecording}
className="text-red-400 hover:text-red-300 p-1"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</>
)}
</>
) : (
// Состояние воспроизведения и отправки
<div className="flex items-center space-x-2 bg-blue-500/20 rounded-lg px-3 py-2">
<Button
type="button"
variant="ghost"
size="sm"
onClick={playRecording}
className="text-blue-300 hover:text-blue-200 p-1"
>
{isPlaying ? <MicOff className="h-3 w-3" /> : <Mic className="h-3 w-3" />}
</Button>
<span className="text-white text-sm font-mono">
{formatDuration(duration)}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={sendVoiceMessage}
className="text-green-400 hover:text-green-300 p-1"
>
<Send className="h-3 w-3" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setRecordedAudio(null)}
className="text-red-400 hover:text-red-300 p-1"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
)}
</div>
)
}