mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 20:51:26 +00:00
feat: mise en place de la messagerie [#17]
This commit is contained in:
233
Front-End/src/components/Chat/MessageInput.js
Normal file
233
Front-End/src/components/Chat/MessageInput.js
Normal file
@ -0,0 +1,233 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Send, Paperclip } from 'lucide-react';
|
||||
import FileUpload from './FileUpload';
|
||||
import { uploadFile } from '@/app/actions/messagerieAction';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const MessageInput = ({
|
||||
onSendMessage,
|
||||
onTypingStart,
|
||||
onTypingStop,
|
||||
disabled = false,
|
||||
placeholder = 'Tapez votre message...',
|
||||
conversationId = null,
|
||||
senderId = null,
|
||||
}) => {
|
||||
const [message, setMessage] = useState('');
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [showFileUpload, setShowFileUpload] = useState(false);
|
||||
const textareaRef = useRef(null);
|
||||
const typingTimeoutRef = useRef(null);
|
||||
|
||||
// Ajuster la hauteur du textarea automatiquement
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`;
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setMessage(value);
|
||||
|
||||
// Gestion du statut de frappe
|
||||
if (value.trim() && !isTyping) {
|
||||
setIsTyping(true);
|
||||
onTypingStart?.();
|
||||
}
|
||||
|
||||
// Réinitialiser le timeout de frappe
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
|
||||
typingTimeoutRef.current = setTimeout(() => {
|
||||
if (isTyping) {
|
||||
setIsTyping(false);
|
||||
onTypingStop?.();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = () => {
|
||||
const trimmedMessage = message.trim();
|
||||
logger.debug('📝 MessageInput: handleSend appelé:', {
|
||||
message,
|
||||
trimmedMessage,
|
||||
disabled,
|
||||
});
|
||||
|
||||
if (!trimmedMessage || disabled) {
|
||||
logger.debug('❌ MessageInput: Message vide ou désactivé');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
'📤 MessageInput: Appel de onSendMessage avec:',
|
||||
trimmedMessage
|
||||
);
|
||||
onSendMessage(trimmedMessage);
|
||||
setMessage('');
|
||||
|
||||
// Arrêter le statut de frappe
|
||||
if (isTyping) {
|
||||
setIsTyping(false);
|
||||
onTypingStop?.();
|
||||
}
|
||||
|
||||
// Effacer le timeout
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setSelectedFile(file);
|
||||
setShowFileUpload(true);
|
||||
}
|
||||
// Réinitialiser l'input file
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const handleFileUpload = async (
|
||||
file,
|
||||
conversationId,
|
||||
senderId,
|
||||
onProgress
|
||||
) => {
|
||||
try {
|
||||
const result = await uploadFile(
|
||||
file,
|
||||
conversationId,
|
||||
senderId,
|
||||
onProgress
|
||||
);
|
||||
|
||||
// Envoyer un message avec le fichier
|
||||
onSendMessage('', {
|
||||
type: 'file',
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: file.type,
|
||||
fileUrl: result.fileUrl,
|
||||
});
|
||||
|
||||
// Réinitialiser l'état
|
||||
setSelectedFile(null);
|
||||
setShowFileUpload(false);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelFileUpload = () => {
|
||||
setSelectedFile(null);
|
||||
setShowFileUpload(false);
|
||||
};
|
||||
|
||||
// Nettoyage du timeout lors du démontage du composant
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="border-t border-gray-200 bg-white">
|
||||
{/* Aperçu d'upload de fichier */}
|
||||
{showFileUpload && selectedFile && (
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<FileUpload
|
||||
file={selectedFile}
|
||||
onUpload={handleFileUpload}
|
||||
onCancel={handleCancelFileUpload}
|
||||
conversationId={conversationId}
|
||||
senderId={senderId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zone de saisie */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-end space-x-3">
|
||||
{/* Zone de saisie */}
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={message}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
style={{ minHeight: '44px', maxHeight: '120px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Boutons à droite (trombone au-dessus, envoi en dessous) */}
|
||||
<div className="flex flex-col space-y-2 flex-shrink-0">
|
||||
{/* Bouton d'ajout de fichier */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
id="file-upload"
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
accept="image/*,application/pdf,.doc,.docx,.xls,.xlsx,.txt"
|
||||
/>
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className="flex items-center justify-center w-10 h-10 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full cursor-pointer transition-colors"
|
||||
>
|
||||
<Paperclip className="w-5 h-5" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Bouton d'envoi */}
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!message.trim() || disabled}
|
||||
className={`flex items-center justify-center w-10 h-10 rounded-full transition-all ${
|
||||
message.trim() && !disabled
|
||||
? 'bg-blue-500 hover:bg-blue-600 text-white shadow-lg hover:shadow-xl'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<Send className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Indicateur de limite de caractères (optionnel) */}
|
||||
{message.length > 900 && (
|
||||
<div className="mt-2 text-right">
|
||||
<span
|
||||
className={`text-xs ${
|
||||
message.length > 1000 ? 'text-red-500' : 'text-yellow-500'
|
||||
}`}
|
||||
>
|
||||
{message.length}/1000
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageInput;
|
||||
Reference in New Issue
Block a user