Files
breakpilot-lehrer/studio-v2/app/messages/_components/ChatArea.tsx
Benjamin Admin 0b37c5e692 [split-required] Split website + studio-v2 monoliths (Phase 3 continued)
Website (14 monoliths split):
- compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20)
- quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11)
- i18n.ts (1,173 → 8 language files)
- unity-bridge (1,094 → 12), backlog (1,087 → 6)
- training (1,066 → 8), rag (1,063 → 8)
- Deleted index_original.ts (4,899 LOC dead backup)

Studio-v2 (5 monoliths split):
- meet/page.tsx (1,481 → 9), messages (1,166 → 9)
- AlertsB2BContext.tsx (1,165 → 5 modules)
- alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6)

All existing imports preserved. Zero new TypeScript errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 17:52:36 +02:00

426 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useRef, useEffect } from 'react'
import {
formatMessageDate,
getContactInitials,
getRoleLabel,
getRoleColor,
type Conversation,
type Message,
type Contact,
} from '@/lib/MessagesContext'
import { EmojiPicker } from './EmojiPicker'
import { TemplatesDropdown } from './TemplatesDropdown'
interface ChatAreaProps {
isDark: boolean
currentConversation: Conversation | null | undefined
currentContact: Contact | undefined
groupedMessages: { date: string; messages: Message[] }[]
messageInput: string
sendWithEmail: boolean
isSending: boolean
showEmojiPicker: boolean
showTemplates: boolean
templates: { id: string; name: string; content: string }[]
setMessageInput: (val: string) => void
setSendWithEmail: (val: boolean) => void
setShowEmojiPicker: (val: boolean) => void
setShowTemplates: (val: boolean) => void
setShowContactInfo: (val: boolean) => void
showContactInfo: boolean
handleSendMessage: () => void
handleEmojiSelect: (emoji: string) => void
handleContextMenu: (e: React.MouseEvent, messageId: string) => void
getSenderName: (senderId: string) => string
pinConversation: (id: string) => void
muteConversation: (id: string) => void
setShowNewConversation: (val: boolean) => void
}
export function ChatArea({
isDark, currentConversation, currentContact, groupedMessages,
messageInput, sendWithEmail, isSending, showEmojiPicker, showTemplates, templates,
setMessageInput, setSendWithEmail, setShowEmojiPicker, setShowTemplates,
setShowContactInfo, showContactInfo,
handleSendMessage, handleEmojiSelect, handleContextMenu,
getSenderName, pinConversation, muteConversation, setShowNewConversation,
}: ChatAreaProps) {
const messagesEndRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [groupedMessages])
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}
return (
<div className={`flex-1 backdrop-blur-xl border rounded-3xl flex flex-col overflow-hidden ${
isDark
? 'bg-white/10 border-white/20'
: 'bg-white/70 border-black/10 shadow-xl'
}`}>
{currentConversation ? (
<>
{/* Chat Header */}
<ChatHeader isDark={isDark} conversation={currentConversation}
contact={currentContact} showContactInfo={showContactInfo}
setShowContactInfo={setShowContactInfo}
pinConversation={pinConversation} muteConversation={muteConversation} />
{/* Messages */}
<div className={`flex-1 overflow-y-auto p-4 space-y-6 ${
isDark
? 'bg-gradient-to-b from-transparent to-black/10'
: 'bg-gradient-to-b from-transparent to-slate-50/50'
}`}>
{groupedMessages.length === 0 ? (
<div className={`text-center py-12 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<div className="w-20 h-20 mx-auto mb-4 rounded-full flex items-center justify-center text-4xl bg-gradient-to-br from-green-500/20 to-emerald-500/20">
👋
</div>
<p className="font-medium text-lg">Noch keine Nachrichten</p>
<p className="text-sm mt-1">Starten Sie die Konversation!</p>
</div>
) : (
groupedMessages.map((group, groupIndex) => (
<MessageGroup key={groupIndex} isDark={isDark} group={group}
currentConversation={currentConversation}
getSenderName={getSenderName}
handleContextMenu={handleContextMenu} />
))
)}
<div ref={messagesEndRef} />
</div>
{/* Message Input */}
<MessageInput isDark={isDark} messageInput={messageInput} sendWithEmail={sendWithEmail}
isSending={isSending} showEmojiPicker={showEmojiPicker} showTemplates={showTemplates}
templates={templates} inputRef={inputRef}
setMessageInput={setMessageInput} setSendWithEmail={setSendWithEmail}
setShowEmojiPicker={setShowEmojiPicker} setShowTemplates={setShowTemplates}
handleSendMessage={handleSendMessage} handleEmojiSelect={handleEmojiSelect}
handleKeyDown={handleKeyDown} />
</>
) : (
<EmptyState isDark={isDark} setShowNewConversation={setShowNewConversation} />
)}
</div>
)
}
// ============================================
// SUB-COMPONENTS
// ============================================
function ChatHeader({ isDark, conversation, contact, showContactInfo, setShowContactInfo, pinConversation, muteConversation }: {
isDark: boolean; conversation: Conversation; contact: Contact | undefined
showContactInfo: boolean; setShowContactInfo: (val: boolean) => void
pinConversation: (id: string) => void; muteConversation: (id: string) => void
}) {
return (
<div className={`p-4 border-b ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="relative">
<div className={`w-12 h-12 rounded-full flex items-center justify-center text-lg font-semibold ${
conversation.is_group
? isDark
? 'bg-gradient-to-br from-purple-500/30 to-pink-500/30 text-purple-300'
: 'bg-gradient-to-br from-purple-100 to-pink-100 text-purple-700'
: contact?.online
? isDark
? 'bg-gradient-to-br from-green-500/30 to-emerald-500/30 text-green-300'
: 'bg-gradient-to-br from-green-100 to-emerald-100 text-green-700'
: isDark
? 'bg-slate-700 text-slate-300'
: 'bg-slate-200 text-slate-600'
}`}>
{conversation.title ? getContactInitials(conversation.title) : '?'}
</div>
{!conversation.is_group && contact?.online && (
<span className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-slate-900" />
)}
</div>
<div>
<h3 className={`font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{conversation.title || 'Unbenannt'}
</h3>
<div className="flex items-center gap-2">
{conversation.typing ? (
<span className={`text-sm ${isDark ? 'text-green-400' : 'text-green-600'}`}>schreibt...</span>
) : contact ? (
<>
<span className={`text-xs px-2 py-0.5 rounded-full ${getRoleColor(contact.role, isDark)}`}>
{getRoleLabel(contact.role)}
</span>
{contact.student_name && (
<span className={`text-xs ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
{contact.student_name}
</span>
)}
{contact.online && (
<span className={`text-xs ${isDark ? 'text-green-400' : 'text-green-600'}`}> Online</span>
)}
</>
) : conversation.is_group && (
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
{conversation.participant_ids.length} Mitglieder
</span>
)}
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
<HeaderButton isDark={isDark} active={conversation.pinned}
activeColor="amber" onClick={() => pinConversation(conversation.id)}
title={conversation.pinned ? 'Nicht mehr anheften' : 'Anheften'}>
<svg className="w-5 h-5" fill={conversation.pinned ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
</HeaderButton>
<HeaderButton isDark={isDark} active={conversation.muted}
activeColor="red" onClick={() => muteConversation(conversation.id)}
title={conversation.muted ? 'Ton aktivieren' : 'Stummschalten'}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{conversation.muted ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15.536 8.464a5 5 0 010 7.072M18.364 5.636a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
)}
</svg>
</HeaderButton>
<HeaderButton isDark={isDark} active={showContactInfo}
activeColor="green" onClick={() => setShowContactInfo(!showContactInfo)}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</HeaderButton>
</div>
</div>
</div>
)
}
function HeaderButton({ isDark, active, activeColor, onClick, title, children }: {
isDark: boolean; active: boolean; activeColor: string
onClick: () => void; title?: string; children: React.ReactNode
}) {
const activeClasses: Record<string, string> = {
amber: isDark ? 'bg-amber-500/20 text-amber-300' : 'bg-amber-100 text-amber-700',
red: isDark ? 'bg-red-500/20 text-red-300' : 'bg-red-100 text-red-700',
green: isDark ? 'bg-green-500/20 text-green-300' : 'bg-green-100 text-green-700',
}
return (
<button onClick={onClick} title={title}
className={`p-2 rounded-xl transition-all ${
active
? activeClasses[activeColor]
: isDark
? 'hover:bg-white/10 text-white/60'
: 'hover:bg-slate-100 text-slate-400'
}`}>
{children}
</button>
)
}
function MessageGroup({ isDark, group, currentConversation, getSenderName, handleContextMenu }: {
isDark: boolean; group: { date: string; messages: Message[] }
currentConversation: Conversation
getSenderName: (id: string) => string
handleContextMenu: (e: React.MouseEvent, id: string) => void
}) {
return (
<div>
<div className="flex items-center justify-center mb-4">
<span className={`px-4 py-1.5 rounded-full text-xs font-medium ${
isDark ? 'bg-white/10 text-white/60' : 'bg-slate-200 text-slate-600'
}`}>
{group.date}
</span>
</div>
<div className="space-y-3">
{group.messages.map((msg) => {
const isSelf = msg.sender_id === 'self'
const isGroupMsg = currentConversation.is_group && !isSelf
return (
<div key={msg.id} className={`flex ${isSelf ? 'justify-end' : 'justify-start'}`}
onContextMenu={(e) => handleContextMenu(e, msg.id)}>
<div className={`max-w-[70%] ${isSelf ? 'order-2' : ''}`}>
{isGroupMsg && (
<span className={`text-xs ml-3 mb-1 block ${isDark ? 'text-purple-400' : 'text-purple-600'}`}>
{getSenderName(msg.sender_id)}
</span>
)}
<div className={`rounded-2xl px-4 py-2.5 shadow-sm ${
isSelf
? 'bg-gradient-to-r from-green-500 to-emerald-500 text-white rounded-br-md'
: isDark
? 'bg-white/10 text-white rounded-bl-md'
: 'bg-white text-slate-900 rounded-bl-md shadow-lg'
}`}>
<p className="whitespace-pre-wrap break-words">{msg.content}</p>
{msg.reactions && msg.reactions.length > 0 && (
<div className="flex gap-1 mt-2">
{msg.reactions.map((r, i) => (
<span key={i} className={`text-sm px-1.5 py-0.5 rounded-full ${isDark ? 'bg-white/20' : 'bg-slate-100'}`}>
{r.emoji}
</span>
))}
</div>
)}
<div className={`flex items-center gap-2 mt-1 ${
isSelf ? 'text-white/70 justify-end' : isDark ? 'text-white/40' : 'text-slate-400'
}`}>
<span className="text-xs">
{new Date(msg.timestamp).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</span>
{isSelf && (
<>
{msg.delivered && (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
{msg.email_sent && <span className="text-xs"></span>}
</>
)}
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
)
}
function MessageInput({ isDark, messageInput, sendWithEmail, isSending, showEmojiPicker, showTemplates, templates, inputRef,
setMessageInput, setSendWithEmail, setShowEmojiPicker, setShowTemplates,
handleSendMessage, handleEmojiSelect, handleKeyDown }: {
isDark: boolean; messageInput: string; sendWithEmail: boolean; isSending: boolean
showEmojiPicker: boolean; showTemplates: boolean
templates: { id: string; name: string; content: string }[]
inputRef: React.RefObject<HTMLTextAreaElement | null>
setMessageInput: (val: string) => void; setSendWithEmail: (val: boolean) => void
setShowEmojiPicker: (val: boolean) => void; setShowTemplates: (val: boolean) => void
handleSendMessage: () => void; handleEmojiSelect: (emoji: string) => void
handleKeyDown: (e: React.KeyboardEvent) => void
}) {
return (
<div className={`p-4 border-t ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
<div className="flex items-center gap-2 mb-3">
<button onClick={() => setSendWithEmail(!sendWithEmail)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-xl text-sm transition-all ${
sendWithEmail
? isDark ? 'bg-blue-500/20 text-blue-300' : 'bg-blue-100 text-blue-700'
: isDark ? 'bg-white/5 text-white/40 hover:bg-white/10' : 'bg-slate-100 text-slate-400 hover:bg-slate-200'
}`}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
E-Mail
</button>
</div>
<div className="flex items-end gap-2">
<div className="relative">
<button onClick={() => { setShowEmojiPicker(!showEmojiPicker); setShowTemplates(false) }}
className={`p-3 rounded-2xl transition-all ${
showEmojiPicker
? isDark ? 'bg-green-500/20 text-green-300' : 'bg-green-100 text-green-700'
: isDark ? 'bg-white/5 text-white/60 hover:bg-white/10' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}>
<span className="text-xl">😊</span>
</button>
{showEmojiPicker && (
<EmojiPicker onSelect={handleEmojiSelect} onClose={() => setShowEmojiPicker(false)} isDark={isDark} />
)}
</div>
<div className="relative">
<button onClick={() => { setShowTemplates(!showTemplates); setShowEmojiPicker(false) }}
className={`p-3 rounded-2xl transition-all ${
showTemplates
? isDark ? 'bg-purple-500/20 text-purple-300' : 'bg-purple-100 text-purple-700'
: isDark ? 'bg-white/5 text-white/60 hover:bg-white/10' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</button>
{showTemplates && (
<TemplatesDropdown templates={templates}
onSelect={(content) => { setMessageInput(content); setShowTemplates(false); inputRef.current?.focus() }}
isDark={isDark} />
)}
</div>
<textarea ref={inputRef} value={messageInput}
onChange={(e) => setMessageInput(e.target.value)} onKeyDown={handleKeyDown}
placeholder="Nachricht schreiben..." rows={1}
className={`flex-1 px-4 py-3 rounded-2xl border resize-none transition-all ${
isDark
? 'bg-white/5 border-white/10 text-white placeholder:text-white/40 focus:border-green-500/50'
: 'bg-white border-slate-200 text-slate-900 placeholder:text-slate-400 focus:border-green-500'
} focus:outline-none focus:ring-2 focus:ring-green-500/20`}
style={{ maxHeight: '120px' }} />
<button onClick={handleSendMessage} disabled={!messageInput.trim() || isSending}
className={`p-3 rounded-2xl transition-all ${
messageInput.trim() && !isSending
? 'bg-gradient-to-r from-green-500 to-emerald-500 text-white shadow-lg hover:shadow-green-500/30'
: isDark
? 'bg-white/5 text-white/30 cursor-not-allowed'
: 'bg-slate-100 text-slate-300 cursor-not-allowed'
}`}>
{isSending ? (
<svg className="w-6 h-6 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
) : (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
)}
</button>
</div>
</div>
)
}
function EmptyState({ isDark, setShowNewConversation }: { isDark: boolean; setShowNewConversation: (val: boolean) => void }) {
return (
<div className="flex-1 flex items-center justify-center">
<div className={`text-center max-w-md px-8 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<div className="w-24 h-24 mx-auto mb-6 rounded-full flex items-center justify-center text-5xl bg-gradient-to-br from-green-500/20 to-emerald-500/20">
💬
</div>
<h3 className={`text-2xl font-bold mb-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>
BreakPilot Messenger
</h3>
<p className="text-sm leading-relaxed">
Kommunizieren Sie sicher mit Eltern und Kollegen.
Waehlen Sie eine Konversation aus der Liste oder starten Sie eine neue Unterhaltung.
</p>
<button onClick={() => setShowNewConversation(true)}
className="mt-6 px-6 py-3 bg-gradient-to-r from-green-500 to-emerald-500 text-white rounded-2xl font-medium shadow-lg hover:shadow-green-500/30 transition-all">
Neue Nachricht
</button>
</div>
</div>
)
}