Files
breakpilot-lehrer/studio-v2/app/messages/page.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

1167 lines
52 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 { useState, useEffect, useRef, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { Sidebar } from '@/components/Sidebar'
import { useLanguage } from '@/lib/LanguageContext'
import { useTheme } from '@/lib/ThemeContext'
import {
useMessages,
formatMessageTime,
formatMessageDate,
getContactInitials,
getRoleLabel,
getRoleColor,
emojiCategories,
type Conversation,
type Message,
type Contact
} from '@/lib/MessagesContext'
// ============================================
// EMOJI PICKER COMPONENT
// ============================================
function EmojiPicker({
onSelect,
onClose,
isDark
}: {
onSelect: (emoji: string) => void
onClose: () => void
isDark: boolean
}) {
const [activeCategory, setActiveCategory] = useState('Häufig')
return (
<div className={`absolute bottom-full left-0 mb-2 w-80 rounded-2xl border shadow-2xl overflow-hidden z-50 ${
isDark
? 'bg-slate-900 border-white/20'
: 'bg-white border-slate-200'
}`}>
{/* Header */}
<div className={`flex items-center justify-between p-3 border-b ${
isDark ? 'border-white/10' : 'border-slate-100'
}`}>
<span className={`text-sm font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
Emoji
</span>
<button
onClick={onClose}
className={`p-1 rounded-lg transition-colors ${
isDark ? 'hover:bg-white/10 text-white/60' : 'hover:bg-slate-100 text-slate-400'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Category Tabs */}
<div className={`flex overflow-x-auto border-b scrollbar-hide ${
isDark ? 'border-white/10' : 'border-slate-100'
}`}>
{Object.keys(emojiCategories).map(cat => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-3 py-2 text-xs font-medium whitespace-nowrap transition-colors ${
activeCategory === cat
? isDark
? 'text-green-400 border-b-2 border-green-400'
: 'text-green-600 border-b-2 border-green-600'
: isDark
? 'text-white/60 hover:text-white'
: 'text-slate-500 hover:text-slate-900'
}`}
>
{cat}
</button>
))}
</div>
{/* Emoji Grid */}
<div className="p-3 max-h-48 overflow-y-auto">
<div className="grid grid-cols-8 gap-1">
{emojiCategories[activeCategory as keyof typeof emojiCategories].map((emoji, i) => (
<button
key={i}
onClick={() => onSelect(emoji)}
className={`w-8 h-8 flex items-center justify-center text-xl rounded-lg transition-colors ${
isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'
}`}
>
{emoji}
</button>
))}
</div>
</div>
</div>
)
}
// ============================================
// MESSAGE TEMPLATES DROPDOWN
// ============================================
function TemplatesDropdown({
templates,
onSelect,
isDark
}: {
templates: { id: string; name: string; content: string }[]
onSelect: (content: string) => void
isDark: boolean
}) {
return (
<div className={`absolute bottom-full left-0 mb-2 w-64 rounded-2xl border shadow-2xl overflow-hidden z-50 ${
isDark
? 'bg-slate-900 border-white/20'
: 'bg-white border-slate-200'
}`}>
<div className={`p-3 border-b ${isDark ? 'border-white/10' : 'border-slate-100'}`}>
<span className={`text-sm font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
Vorlagen
</span>
</div>
<div className="max-h-64 overflow-y-auto">
{templates.map(tpl => (
<button
key={tpl.id}
onClick={() => onSelect(tpl.content)}
className={`w-full text-left p-3 transition-colors ${
isDark
? 'hover:bg-white/5 border-b border-white/5'
: 'hover:bg-slate-50 border-b border-slate-100'
}`}
>
<span className={`text-sm font-medium block ${isDark ? 'text-white' : 'text-slate-900'}`}>
{tpl.name}
</span>
<span className={`text-xs line-clamp-2 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
{tpl.content}
</span>
</button>
))}
</div>
</div>
)
}
// ============================================
// CONTACT INFO PANEL
// ============================================
function ContactInfoPanel({
contact,
conversation,
onClose,
isDark
}: {
contact: Contact | undefined
conversation: Conversation
onClose: () => void
isDark: boolean
}) {
return (
<div className={`w-80 backdrop-blur-2xl border-l flex flex-col ${
isDark
? 'bg-white/5 border-white/10'
: 'bg-white/90 border-slate-200'
}`}>
{/* Header */}
<div className={`p-4 border-b ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
<div className="flex items-center justify-between">
<span className={`font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>Info</span>
<button
onClick={onClose}
className={`p-2 rounded-xl transition-colors ${
isDark ? 'hover:bg-white/10 text-white/60' : 'hover:bg-slate-100 text-slate-400'
}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{/* Avatar & Name */}
<div className="text-center mb-6">
<div className={`w-20 h-20 mx-auto rounded-full flex items-center justify-center text-2xl font-bold mb-3 ${
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'
}`}>
{conversation.title ? getContactInitials(conversation.title) : '?'}
</div>
<h3 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{conversation.title}
</h3>
{contact && (
<span className={`text-sm px-2 py-1 rounded-full ${getRoleColor(contact.role, isDark)}`}>
{getRoleLabel(contact.role)}
</span>
)}
</div>
{/* Contact Details */}
{contact && (
<div className="space-y-4">
{contact.email && (
<div className={`p-3 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
<span className={`text-xs block mb-1 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>E-Mail</span>
<span className={`text-sm ${isDark ? 'text-white' : 'text-slate-900'}`}>{contact.email}</span>
</div>
)}
{contact.phone && (
<div className={`p-3 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
<span className={`text-xs block mb-1 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>Telefon</span>
<span className={`text-sm ${isDark ? 'text-white' : 'text-slate-900'}`}>{contact.phone}</span>
</div>
)}
{contact.student_name && (
<div className={`p-3 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
<span className={`text-xs block mb-1 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>Schueler/in</span>
<span className={`text-sm ${isDark ? 'text-white' : 'text-slate-900'}`}>
{contact.student_name} ({contact.class_name})
</span>
</div>
)}
{contact.tags.length > 0 && (
<div className={`p-3 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
<span className={`text-xs block mb-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>Tags</span>
<div className="flex flex-wrap gap-1">
{contact.tags.map(tag => (
<span key={tag} className={`text-xs px-2 py-1 rounded-full ${
isDark ? 'bg-white/10 text-white/80' : 'bg-slate-200 text-slate-700'
}`}>
{tag}
</span>
))}
</div>
</div>
)}
</div>
)}
{/* Group Members */}
{conversation.is_group && (
<div className={`p-3 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
<span className={`text-xs block mb-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{conversation.participant_ids.length} Mitglieder
</span>
</div>
)}
</div>
</div>
)
}
// ============================================
// MAIN PAGE
// ============================================
export default function MessagesPage() {
const { t } = useLanguage()
const { isDark } = useTheme()
const router = useRouter()
const {
contacts,
conversations,
messages,
templates,
unreadCount,
recentConversations,
sendMessage,
markAsRead,
createConversation,
addReaction,
deleteMessage,
pinConversation,
muteConversation,
currentConversationId,
setCurrentConversationId
} = useMessages()
const [messageInput, setMessageInput] = useState('')
const [sendWithEmail, setSendWithEmail] = useState(false)
const [isSending, setIsSending] = useState(false)
const [showNewConversation, setShowNewConversation] = useState(false)
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [showTemplates, setShowTemplates] = useState(false)
const [showContactInfo, setShowContactInfo] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; messageId: string } | null>(null)
const messagesEndRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
// Current conversation data
const currentConversation = conversations.find(c => c.id === currentConversationId)
const currentMessages = currentConversationId ? (messages[currentConversationId] || []) : []
// Find contact for conversation
const getConversationContact = (conv: Conversation): Contact | undefined => {
if (conv.is_group) return undefined
return contacts.find(c => conv.participant_ids.includes(c.id))
}
// Get sender name for group messages
const getSenderName = (senderId: string): string => {
if (senderId === 'self') return 'Du'
const contact = contacts.find(c => c.id === senderId)
return contact?.name?.split(' ')[0] || 'Unbekannt'
}
// Filter conversations by search
const filteredConversations = useMemo(() => {
if (!searchQuery) return recentConversations
const q = searchQuery.toLowerCase()
return recentConversations.filter(c =>
c.title?.toLowerCase().includes(q) ||
c.last_message?.toLowerCase().includes(q)
)
}, [recentConversations, searchQuery])
// Group messages by date
const groupedMessages = useMemo(() => {
const groups: { date: string; messages: Message[] }[] = []
let currentDate = ''
for (const msg of currentMessages) {
const msgDate = formatMessageDate(msg.timestamp)
if (msgDate !== currentDate) {
currentDate = msgDate
groups.push({ date: msgDate, messages: [] })
}
groups[groups.length - 1].messages.push(msg)
}
return groups
}, [currentMessages])
// Select conversation
const selectConversation = async (conv: Conversation) => {
setCurrentConversationId(conv.id)
if (conv.unread_count > 0) {
await markAsRead(conv.id)
}
setShowContactInfo(false)
}
// Send message
const handleSendMessage = async () => {
if (!messageInput.trim() || !currentConversationId) return
setIsSending(true)
await sendMessage(currentConversationId, messageInput.trim(), sendWithEmail)
setMessageInput('')
setIsSending(false)
setShowEmojiPicker(false)
setShowTemplates(false)
inputRef.current?.focus()
}
// Insert emoji
const handleEmojiSelect = (emoji: string) => {
setMessageInput(prev => prev + emoji)
inputRef.current?.focus()
}
// Start new conversation
const handleStartConversation = async (contact: Contact) => {
const conv = await createConversation(contact.id)
if (conv) {
setCurrentConversationId(conv.id)
setShowNewConversation(false)
}
}
// Handle context menu
const handleContextMenu = (e: React.MouseEvent, messageId: string) => {
e.preventDefault()
setContextMenu({ x: e.clientX, y: e.clientY, messageId })
}
// Scroll to bottom
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [currentMessages])
// Close context menu on click outside
useEffect(() => {
const handleClick = () => setContextMenu(null)
if (contextMenu) {
document.addEventListener('click', handleClick)
return () => document.removeEventListener('click', handleClick)
}
}, [contextMenu])
// Handle Enter key
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}
const currentContact = currentConversation ? getConversationContact(currentConversation) : undefined
return (
<div className={`min-h-screen relative overflow-hidden ${
isDark
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
: 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'
}`}>
{/* Animated Background Blobs */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className={`absolute -top-40 -right-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${
isDark ? 'bg-purple-500 opacity-70' : 'bg-purple-300 opacity-50'
}`} />
<div className={`absolute -bottom-40 -left-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${
isDark ? 'bg-blue-500 opacity-70' : 'bg-blue-300 opacity-50'
}`} />
<div className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${
isDark ? 'bg-green-500 opacity-70' : 'bg-green-300 opacity-50'
}`} />
</div>
<div className="relative z-10 flex min-h-screen gap-4 p-4">
{/* Sidebar */}
<Sidebar selectedTab="messages" />
{/* Main Content */}
<main className="flex-1 flex gap-4 h-[calc(100vh-32px)]">
{/* Conversations List */}
<div className={`w-96 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'
}`}>
{/* Header */}
<div className={`p-4 border-b ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
<div className="flex items-center justify-between mb-4">
<div>
<h2 className={`text-xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
Nachrichten
</h2>
{unreadCount > 0 && (
<span className={`text-sm ${isDark ? 'text-green-400' : 'text-green-600'}`}>
{unreadCount} ungelesen
</span>
)}
</div>
<button
onClick={() => setShowNewConversation(true)}
className={`p-3 rounded-2xl transition-all shadow-lg ${
isDark
? 'bg-gradient-to-r from-green-500 to-emerald-500 text-white hover:shadow-green-500/30'
: 'bg-gradient-to-r from-green-500 to-emerald-500 text-white hover:shadow-green-500/30'
}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</button>
</div>
{/* Search */}
<div className="relative">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Suchen..."
className={`w-full px-4 py-3 pl-10 rounded-2xl border 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`}
/>
<svg className={`absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 ${
isDark ? 'text-white/40' : 'text-slate-400'
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
{/* Conversation List */}
<div className="flex-1 overflow-y-auto">
{filteredConversations.length === 0 ? (
<div className={`p-8 text-center ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<div className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center text-3xl bg-gradient-to-br from-green-500/20 to-emerald-500/20">
💬
</div>
<p className="font-medium">Keine Konversationen</p>
<p className="text-sm mt-1">Starten Sie eine neue Unterhaltung!</p>
</div>
) : (
<div className="divide-y divide-white/5">
{filteredConversations.map((conv) => {
const contact = getConversationContact(conv)
const isActive = currentConversationId === conv.id
return (
<button
key={conv.id}
onClick={() => selectConversation(conv)}
className={`w-full p-4 text-left transition-all ${
isActive
? isDark
? 'bg-gradient-to-r from-green-500/20 to-emerald-500/20'
: 'bg-gradient-to-r from-green-100 to-emerald-100'
: isDark
? 'hover:bg-white/5'
: 'hover:bg-slate-50'
}`}
>
<div className="flex items-start gap-3">
{/* Avatar */}
<div className="relative">
<div className={`w-12 h-12 rounded-full flex items-center justify-center text-lg font-semibold ${
conv.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'
}`}>
{conv.title ? getContactInitials(conv.title) : '?'}
</div>
{!conv.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>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
{conv.pinned && (
<span className="text-xs">📌</span>
)}
<span className={`font-semibold truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
{conv.title || 'Unbenannt'}
</span>
{conv.muted && (
<span className={`${isDark ? 'text-white/40' : 'text-slate-400'}`}>🔕</span>
)}
</div>
{conv.last_message_time && (
<span className={`text-xs flex-shrink-0 ${
conv.unread_count > 0
? isDark ? 'text-green-400' : 'text-green-600'
: isDark ? 'text-white/40' : 'text-slate-400'
}`}>
{formatMessageTime(conv.last_message_time)}
</span>
)}
</div>
{/* Last Message */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 flex-1 min-w-0">
{conv.typing ? (
<span className={`text-sm italic ${isDark ? 'text-green-400' : 'text-green-600'}`}>
schreibt...
</span>
) : (
<p className={`text-sm truncate ${
conv.unread_count > 0
? isDark ? 'text-white font-medium' : 'text-slate-900 font-medium'
: isDark ? 'text-white/60' : 'text-slate-500'
}`}>
{conv.last_message}
</p>
)}
</div>
{conv.unread_count > 0 && (
<span className="ml-2 min-w-[20px] h-5 px-1.5 rounded-full bg-green-500 text-white text-xs flex items-center justify-center font-medium">
{conv.unread_count > 9 ? '9+' : conv.unread_count}
</span>
)}
</div>
</div>
</div>
</button>
)
})}
</div>
)}
</div>
</div>
{/* Chat Area */}
<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 */}
<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">
{/* Avatar */}
<div className="relative">
<div className={`w-12 h-12 rounded-full flex items-center justify-center text-lg font-semibold ${
currentConversation.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'
: currentContact?.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'
}`}>
{currentConversation.title ? getContactInitials(currentConversation.title) : '?'}
</div>
{!currentConversation.is_group && currentContact?.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'}`}>
{currentConversation.title || 'Unbenannt'}
</h3>
<div className="flex items-center gap-2">
{currentConversation.typing ? (
<span className={`text-sm ${isDark ? 'text-green-400' : 'text-green-600'}`}>
schreibt...
</span>
) : currentContact ? (
<>
<span className={`text-xs px-2 py-0.5 rounded-full ${getRoleColor(currentContact.role, isDark)}`}>
{getRoleLabel(currentContact.role)}
</span>
{currentContact.student_name && (
<span className={`text-xs ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
{currentContact.student_name}
</span>
)}
{currentContact.online && (
<span className={`text-xs ${isDark ? 'text-green-400' : 'text-green-600'}`}>
Online
</span>
)}
</>
) : currentConversation.is_group && (
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
{currentConversation.participant_ids.length} Mitglieder
</span>
)}
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
<button
onClick={() => pinConversation(currentConversation.id)}
className={`p-2 rounded-xl transition-all ${
currentConversation.pinned
? isDark
? 'bg-amber-500/20 text-amber-300'
: 'bg-amber-100 text-amber-700'
: isDark
? 'hover:bg-white/10 text-white/60'
: 'hover:bg-slate-100 text-slate-400'
}`}
title={currentConversation.pinned ? 'Nicht mehr anheften' : 'Anheften'}
>
<svg className="w-5 h-5" fill={currentConversation.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>
</button>
<button
onClick={() => muteConversation(currentConversation.id)}
className={`p-2 rounded-xl transition-all ${
currentConversation.muted
? isDark
? 'bg-red-500/20 text-red-300'
: 'bg-red-100 text-red-700'
: isDark
? 'hover:bg-white/10 text-white/60'
: 'hover:bg-slate-100 text-slate-400'
}`}
title={currentConversation.muted ? 'Ton aktivieren' : 'Stummschalten'}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{currentConversation.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>
</button>
<button
onClick={() => setShowContactInfo(!showContactInfo)}
className={`p-2 rounded-xl transition-all ${
showContactInfo
? isDark
? 'bg-green-500/20 text-green-300'
: 'bg-green-100 text-green-700'
: isDark
? 'hover:bg-white/10 text-white/60'
: 'hover:bg-slate-100 text-slate-400'
}`}
>
<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>
</button>
</div>
</div>
</div>
{/* 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) => (
<div key={groupIndex}>
{/* Date Separator */}
<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>
{/* Messages */}
<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' : ''}`}>
{/* Sender name for groups */}
{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>
{/* Reactions */}
{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>
)}
{/* Time & Status */}
<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>
))
)}
<div ref={messagesEndRef} />
</div>
{/* Message Input */}
<div className={`p-4 border-t ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
{/* Options Row */}
<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>
{/* Input Row */}
<div className="flex items-end gap-2">
{/* Emoji Button */}
<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>
{/* Templates Button */}
<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>
{/* Text Input */}
<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' }}
/>
{/* Send Button */}
<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>
</>
) : (
/* No Conversation Selected */
<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>
)}
</div>
{/* Contact Info Panel */}
{showContactInfo && currentConversation && (
<ContactInfoPanel
contact={currentContact}
conversation={currentConversation}
onClose={() => setShowContactInfo(false)}
isDark={isDark}
/>
)}
</main>
</div>
{/* New Conversation Modal */}
{showNewConversation && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className={`w-full max-w-md backdrop-blur-2xl border rounded-3xl overflow-hidden shadow-2xl ${
isDark
? 'bg-slate-900/90 border-white/10'
: 'bg-white border-slate-200'
}`}>
{/* Header */}
<div className={`p-6 border-b ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
<div className="flex items-center justify-between">
<h3 className={`text-xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
Neue Nachricht
</h3>
<button
onClick={() => setShowNewConversation(false)}
className={`p-2 rounded-xl transition-all ${
isDark
? 'hover:bg-white/10 text-white/60'
: 'hover:bg-slate-100 text-slate-400'
}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Contact List */}
<div className="max-h-96 overflow-y-auto">
{contacts.length === 0 ? (
<div className={`p-8 text-center ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<p>Keine Kontakte vorhanden</p>
</div>
) : (
<div>
{contacts.map((contact) => (
<button
key={contact.id}
onClick={() => handleStartConversation(contact)}
className={`w-full p-4 text-left transition-all flex items-center gap-3 ${
isDark
? 'hover:bg-white/5 border-b border-white/5'
: 'hover:bg-slate-50 border-b border-slate-100'
}`}
>
<div className="relative">
<div className={`w-12 h-12 rounded-full flex items-center justify-center text-lg font-semibold ${
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'
}`}>
{getContactInitials(contact.name)}
</div>
{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 className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className={`font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{contact.name}
</span>
<span className={`text-xs px-2 py-0.5 rounded-full ${getRoleColor(contact.role, isDark)}`}>
{getRoleLabel(contact.role)}
</span>
</div>
{contact.student_name && (
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
{contact.student_name} ({contact.class_name})
</p>
)}
{contact.email && (
<p className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{contact.email}
</p>
)}
</div>
</button>
))}
</div>
)}
</div>
</div>
</div>
)}
{/* Context Menu */}
{contextMenu && (
<div
className={`fixed rounded-xl border shadow-xl overflow-hidden z-50 ${
isDark
? 'bg-slate-900 border-white/20'
: 'bg-white border-slate-200'
}`}
style={{ top: contextMenu.y, left: contextMenu.x }}
>
<button
onClick={() => {
const quickReactions = ['👍', '❤️', '😊', '😂', '🙏']
// Show quick reactions
setContextMenu(null)
}}
className={`w-full px-4 py-2 text-left text-sm transition-colors ${
isDark ? 'hover:bg-white/10 text-white' : 'hover:bg-slate-100 text-slate-700'
}`}
>
Reagieren
</button>
<button
onClick={() => {
if (currentConversationId) {
deleteMessage(currentConversationId, contextMenu.messageId)
}
setContextMenu(null)
}}
className={`w-full px-4 py-2 text-left text-sm transition-colors ${
isDark ? 'hover:bg-red-500/20 text-red-400' : 'hover:bg-red-50 text-red-600'
}`}
>
Loeschen
</button>
</div>
)}
{/* Blob Animation Styles */}
<style jsx>{`
@keyframes blob {
0% { transform: translate(0px, 0px) scale(1); }
33% { transform: translate(30px, -50px) scale(1.1); }
66% { transform: translate(-20px, 20px) scale(0.9); }
100% { transform: translate(0px, 0px) scale(1); }
}
.animate-blob {
animation: blob 7s infinite;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
`}</style>
</div>
)
}