Merge pull request #6 from metacube2/codex/implement-new-chat-age-restrictions
Sonniges UI und stabilere Chat-Streams
This commit is contained in:
@@ -1470,14 +1470,30 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sun-50: #fff9db;
|
||||||
|
--sun-100: #fef3c7;
|
||||||
|
--sun-200: #fde68a;
|
||||||
|
--sun-300: #fcd34d;
|
||||||
|
--sun-400: #fbbf24;
|
||||||
|
--sun-500: #f59e0b;
|
||||||
|
--sun-600: #d97706;
|
||||||
|
--sun-700: #b45309;
|
||||||
|
--sun-800: #92400e;
|
||||||
|
--sun-900: #78350f;
|
||||||
|
--text-dark: #3d2c00;
|
||||||
|
--text-muted: rgba(61, 44, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #fef08a 0%, #f97316 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
color: var(--text-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════════ */
|
/* ═══════════════════════════════════════════════════════════ */
|
||||||
@@ -1485,7 +1501,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
/* ═══════════════════════════════════════════════════════════ */
|
/* ═══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
.auth-container {
|
.auth-container {
|
||||||
background: white;
|
background: #fff9db;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
@@ -1494,35 +1510,35 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auth-container h1 {
|
.auth-container h1 {
|
||||||
color: #667eea;
|
color: #d97706;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-container .subtitle {
|
.auth-container .subtitle {
|
||||||
color: #666;
|
color: #7c4a03;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-container .warning-box {
|
.auth-container .warning-box {
|
||||||
background: #fff3cd;
|
background: #fef3c7;
|
||||||
border: 2px solid #ffc107;
|
border: 2px solid #fbbf24;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-container .warning-box h3 {
|
.auth-container .warning-box h3 {
|
||||||
color: #856404;
|
color: #a16207;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-container .warning-box ul {
|
.auth-container .warning-box ul {
|
||||||
color: #856404;
|
color: #a16207;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@@ -1534,7 +1550,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
|
|
||||||
.auth-container label {
|
.auth-container label {
|
||||||
display: block;
|
display: block;
|
||||||
color: #333;
|
color: #7c4a03;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -1545,15 +1561,16 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
.auth-container input[type="password"] {
|
.auth-container input[type="password"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 15px;
|
padding: 12px 15px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid #fde68a;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
transition: border-color 0.3s;
|
transition: border-color 0.3s, box-shadow 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-container input:focus {
|
.auth-container input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #667eea;
|
border-color: #f59e0b;
|
||||||
|
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-container .checkbox-group {
|
.auth-container .checkbox-group {
|
||||||
@@ -1579,11 +1596,11 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
|
|
||||||
.auth-container .terms-text {
|
.auth-container .terms-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #7c4a03;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #f8f9fa;
|
background: #fff4cc;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -1592,18 +1609,19 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
.auth-container button {
|
.auth-container button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #fbbf24 0%, #f97316 100%);
|
||||||
color: white;
|
color: #3d2c00;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-container button:hover {
|
.auth-container button:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 12px 24px rgba(249, 115, 22, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-container button:disabled {
|
.auth-container button:disabled {
|
||||||
@@ -1616,24 +1634,25 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
/* ═══════════════════════════════════════════════════════════ */
|
/* ═══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
.admin-login-container {
|
.admin-login-container {
|
||||||
background: white;
|
background: var(--sun-50);
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
box-shadow: 0 20px 60px rgba(120, 53, 15, 0.25);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 450px;
|
max-width: 450px;
|
||||||
|
color: var(--text-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-login-container h1 {
|
.admin-login-container h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
color: #4c51bf;
|
color: var(--sun-800);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-login-container p {
|
.admin-login-container p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #666;
|
color: var(--text-muted);
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1645,21 +1664,22 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: var(--sun-800);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-login-container input {
|
.admin-login-container input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--sun-200);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
transition: border-color 0.2s ease;
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-login-container input:focus {
|
.admin-login-container input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #667eea;
|
border-color: var(--sun-600);
|
||||||
|
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-login-container button {
|
.admin-login-container button {
|
||||||
@@ -1667,16 +1687,17 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, var(--sun-400) 0%, var(--sun-600) 100%);
|
||||||
color: white;
|
color: var(--text-dark);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-login-container button:hover {
|
.admin-login-container button:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 12px 24px rgba(217, 119, 6, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-login-container .back-link {
|
.admin-login-container .back-link {
|
||||||
@@ -1685,7 +1706,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-login-container .back-link a {
|
.admin-login-container .back-link a {
|
||||||
color: #667eea;
|
color: var(--sun-700);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -1697,13 +1718,14 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
.admin-dashboard {
|
.admin-dashboard {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
background: white;
|
background: var(--sun-50);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
box-shadow: 0 20px 60px rgba(120, 53, 15, 0.25);
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
|
color: var(--text-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard-header {
|
.admin-dashboard-header {
|
||||||
@@ -1715,21 +1737,24 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
|
|
||||||
.admin-dashboard-header h1 {
|
.admin-dashboard-header h1 {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
color: #4c51bf;
|
color: var(--sun-800);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard-header button {
|
.admin-dashboard-header button {
|
||||||
padding: 10px 18px;
|
padding: 10px 18px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #ef4444;
|
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-shadow: 0 10px 24px rgba(239, 68, 68, 0.35);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard-header button:hover {
|
.admin-dashboard-header button:hover {
|
||||||
background: #dc2626;
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 12px 28px rgba(220, 38, 38, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-stats-grid {
|
.admin-stats-grid {
|
||||||
@@ -1741,12 +1766,12 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
.admin-stat-card {
|
.admin-stat-card {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, var(--sun-400) 0%, var(--sun-700) 100%);
|
||||||
color: white;
|
color: var(--text-dark);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
box-shadow: 0 12px 30px rgba(102, 126, 234, 0.35);
|
box-shadow: 0 12px 30px rgba(250, 204, 21, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-stat-card span {
|
.admin-stat-card span {
|
||||||
@@ -1766,16 +1791,17 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-section {
|
.admin-section {
|
||||||
background: #f9fafb;
|
background: rgba(255, 255, 255, 0.9);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid rgba(180, 83, 9, 0.2);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 200, 92, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-section h2 {
|
.admin-section h2 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
color: #1f2937;
|
color: var(--sun-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-table-wrapper {
|
.admin-table-wrapper {
|
||||||
@@ -1799,7 +1825,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
color: #6b7280;
|
color: var(--sun-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-action-buttons {
|
.admin-action-buttons {
|
||||||
@@ -2267,7 +2293,8 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
.empty-user-list,
|
.empty-user-list,
|
||||||
.empty-messages,
|
.empty-messages,
|
||||||
.loading-state,
|
.loading-state,
|
||||||
.error-state {
|
.error-state,
|
||||||
|
.chat-state-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 30px 20px;
|
padding: 30px 20px;
|
||||||
color: rgba(60, 42, 0, 0.6);
|
color: rgba(60, 42, 0, 0.6);
|
||||||
@@ -2285,6 +2312,22 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
.error-state {
|
.error-state {
|
||||||
color: #c2410c;
|
color: #c2410c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-state-message.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-state-message.loading-state {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-state-message.error-state {
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -2430,6 +2473,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
<!-- Populated via JS -->
|
<!-- Populated via JS -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-state-message hidden" id="chatStateMessage"></div>
|
||||||
<div class="chat-messages" id="chatMessages">
|
<div class="chat-messages" id="chatMessages">
|
||||||
<!-- Messages loaded via JS -->
|
<!-- Messages loaded via JS -->
|
||||||
</div>
|
</div>
|
||||||
@@ -2798,72 +2842,265 @@ const state = {
|
|||||||
users: [],
|
users: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
lastMessageId: 0,
|
lastMessageId: 0,
|
||||||
eventSource: null
|
eventSource: null,
|
||||||
|
isLoadingUsers: false,
|
||||||
|
isLoadingMessages: false,
|
||||||
|
connectionErrorShown: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userListEl = document.getElementById('userList');
|
||||||
|
const userSearchInput = document.getElementById('userSearch');
|
||||||
|
const chatWelcomeEl = document.getElementById('chatWelcome');
|
||||||
|
const chatMessagesContainerEl = document.getElementById('chatMessagesContainer');
|
||||||
|
const chatMessagesEl = document.getElementById('chatMessages');
|
||||||
|
const chatStateMessageEl = document.getElementById('chatStateMessage');
|
||||||
|
const chatMessagesHeaderEl = document.getElementById('chatMessagesHeader');
|
||||||
|
const chatInputEl = document.getElementById('chatInput');
|
||||||
|
const sendButtonEl = document.getElementById('sendButton');
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
|
if (!userListEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isLoadingUsers = true;
|
||||||
|
renderUserList();
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await fetch('?action=get_users');
|
const response = await fetch('?action=get_users');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('NETZWERK_FEHLER');
|
||||||
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
state.users = result.users;
|
state.users = Array.isArray(result.users) ? result.users : [];
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Nutzerliste konnte nicht geladen werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isLoadingUsers = false;
|
||||||
renderUserList();
|
renderUserList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Nutzerliste konnte nicht geladen werden:', error);
|
||||||
|
state.isLoadingUsers = false;
|
||||||
|
if (userListEl) {
|
||||||
|
userListEl.innerHTML = '<div class="error-state">Nutzerliste konnte nicht geladen werden.</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUserList() {
|
function renderUserList() {
|
||||||
const userList = document.getElementById('userList');
|
if (!userListEl) {
|
||||||
const searchTerm = document.getElementById('userSearch').value.toLowerCase();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const filtered = state.users.filter(u => u.display_name.toLowerCase().includes(searchTerm));
|
const searchTerm = (userSearchInput?.value || '').toLowerCase();
|
||||||
|
const users = Array.isArray(state.users) ? state.users : [];
|
||||||
|
|
||||||
userList.innerHTML = filtered.map(user => `
|
if (state.isLoadingUsers && users.length === 0) {
|
||||||
<div class="user-item ${user.id === state.selectedUserId ? 'active' : ''}" onclick="selectUser(${user.id}, '${user.display_name}')">
|
userListEl.innerHTML = '<div class="loading-state">Nutzer werden geladen…</div>';
|
||||||
<div class="user-avatar">
|
return;
|
||||||
${user.username.charAt(0).toUpperCase()}
|
}
|
||||||
<div class="online-indicator ${user.is_online ? '' : 'offline-indicator'}"></div>
|
|
||||||
</div>
|
if (users.length === 0) {
|
||||||
<div class="user-info-text">
|
userListEl.innerHTML = '<div class="empty-user-list">Noch keine passenden Kontakte verfügbar.</div>';
|
||||||
<div class="user-name">${user.display_name}</div>
|
return;
|
||||||
<div class="user-status">${user.is_online ? 'Online' : 'Offline'}</div>
|
}
|
||||||
</div>
|
|
||||||
${user.unread_count > 0 ? `<div class="unread-badge">${user.unread_count}</div>` : ''}
|
const filtered = users.filter(user => user.display_name.toLowerCase().includes(searchTerm));
|
||||||
</div>
|
|
||||||
`).join('');
|
if (filtered.length === 0) {
|
||||||
|
userListEl.innerHTML = '<div class="empty-user-list">Keine Treffer für deine Suche.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
filtered.forEach(user => {
|
||||||
|
const item = document.createElement('button');
|
||||||
|
item.type = 'button';
|
||||||
|
item.className = 'user-item' + (Number(user.id) === Number(state.selectedUserId) ? ' active' : '');
|
||||||
|
item.dataset.userId = String(user.id);
|
||||||
|
item.dataset.displayName = user.display_name;
|
||||||
|
|
||||||
|
const avatar = document.createElement('div');
|
||||||
|
avatar.className = 'user-avatar';
|
||||||
|
avatar.textContent = (user.username || '?').charAt(0).toUpperCase();
|
||||||
|
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.className = 'online-indicator' + (user.is_online ? '' : ' offline-indicator');
|
||||||
|
avatar.appendChild(indicator);
|
||||||
|
|
||||||
|
const infoWrapper = document.createElement('div');
|
||||||
|
infoWrapper.className = 'user-info-text';
|
||||||
|
|
||||||
|
const name = document.createElement('div');
|
||||||
|
name.className = 'user-name';
|
||||||
|
name.textContent = user.display_name;
|
||||||
|
|
||||||
|
const status = document.createElement('div');
|
||||||
|
status.className = 'user-status';
|
||||||
|
status.textContent = user.is_online ? 'Online' : 'Offline';
|
||||||
|
|
||||||
|
infoWrapper.appendChild(name);
|
||||||
|
infoWrapper.appendChild(status);
|
||||||
|
|
||||||
|
item.appendChild(avatar);
|
||||||
|
item.appendChild(infoWrapper);
|
||||||
|
|
||||||
|
if (Number(user.unread_count) > 0) {
|
||||||
|
const unread = document.createElement('div');
|
||||||
|
unread.className = 'unread-badge';
|
||||||
|
unread.textContent = String(user.unread_count);
|
||||||
|
item.appendChild(unread);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
selectUser(Number(user.id), user.display_name);
|
||||||
|
});
|
||||||
|
|
||||||
|
fragment.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
userListEl.innerHTML = '';
|
||||||
|
userListEl.appendChild(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChatHeader(displayName) {
|
||||||
|
if (!chatMessagesHeaderEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatMessagesHeaderEl.innerHTML = '';
|
||||||
|
|
||||||
|
const avatar = document.createElement('div');
|
||||||
|
avatar.className = 'user-avatar';
|
||||||
|
const initial = (displayName?.trim() || '?').charAt(0).toUpperCase();
|
||||||
|
avatar.textContent = initial || '?';
|
||||||
|
|
||||||
|
const info = document.createElement('div');
|
||||||
|
const name = document.createElement('div');
|
||||||
|
name.className = 'user-name';
|
||||||
|
name.textContent = displayName;
|
||||||
|
info.appendChild(name);
|
||||||
|
|
||||||
|
chatMessagesHeaderEl.appendChild(avatar);
|
||||||
|
chatMessagesHeaderEl.appendChild(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChatState(type, message = '') {
|
||||||
|
if (!chatStateMessageEl || !chatMessagesEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatStateMessageEl.className = 'chat-state-message';
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
chatStateMessageEl.textContent = '';
|
||||||
|
chatStateMessageEl.classList.add('hidden');
|
||||||
|
chatMessagesEl.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatStateMessageEl.textContent = message;
|
||||||
|
chatStateMessageEl.classList.remove('hidden');
|
||||||
|
|
||||||
|
if (type === 'loading') {
|
||||||
|
chatStateMessageEl.classList.add('loading-state');
|
||||||
|
} else if (type === 'error') {
|
||||||
|
chatStateMessageEl.classList.add('error-state');
|
||||||
|
} else if (type === 'empty') {
|
||||||
|
chatStateMessageEl.classList.add('empty-messages');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideMessages = type === 'loading' || type === 'error' || type === 'empty';
|
||||||
|
chatMessagesEl.classList.toggle('hidden', hideMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectUser(userId, displayName) {
|
function selectUser(userId, displayName) {
|
||||||
state.selectedUserId = userId;
|
state.selectedUserId = userId;
|
||||||
|
state.messages = [];
|
||||||
|
|
||||||
document.getElementById('chatWelcome').style.display = 'none';
|
if (chatWelcomeEl) {
|
||||||
document.getElementById('chatMessagesContainer').style.display = 'flex';
|
chatWelcomeEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('chatMessagesHeader').innerHTML = `
|
if (chatMessagesContainerEl) {
|
||||||
<div class="user-avatar">${displayName.charAt(0).toUpperCase()}</div>
|
chatMessagesContainerEl.style.display = 'flex';
|
||||||
<div><div class="user-name">${displayName}</div></div>
|
}
|
||||||
`;
|
|
||||||
|
|
||||||
loadMessages(userId);
|
if (chatMessagesEl) {
|
||||||
|
chatMessagesEl.innerHTML = '';
|
||||||
|
chatMessagesEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChatHeader(displayName);
|
||||||
|
updateChatState('loading', 'Nachrichten werden geladen…');
|
||||||
renderUserList();
|
renderUserList();
|
||||||
|
loadMessages(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMessages(userId) {
|
async function loadMessages(userId) {
|
||||||
|
if (!userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isLoadingMessages = true;
|
||||||
|
updateChatState('loading', 'Nachrichten werden geladen…');
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await fetch(`?action=get_messages&user_id=${userId}`);
|
const response = await fetch(`?action=get_messages&user_id=${userId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('NETZWERK_FEHLER');
|
||||||
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (!result.success) {
|
||||||
state.messages = result.messages;
|
throw new Error(result.error || 'Nachrichten konnten nicht geladen werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.messages = Array.isArray(result.messages) ? result.messages : [];
|
||||||
|
|
||||||
|
if (state.messages.length > 0) {
|
||||||
renderMessages();
|
renderMessages();
|
||||||
markAsRead(userId);
|
markAsRead(userId);
|
||||||
|
const newLastMessageId = Math.max(...state.messages.map(m => Number(m.id)));
|
||||||
if (result.messages.length > 0) {
|
state.lastMessageId = Math.max(state.lastMessageId, newLastMessageId);
|
||||||
state.lastMessageId = Math.max(...result.messages.map(m => m.id));
|
} else {
|
||||||
|
if (chatMessagesEl) {
|
||||||
|
chatMessagesEl.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
updateChatState('empty', 'Noch keine Nachrichten. Starte das Gespräch!');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Nachrichten konnten nicht geladen werden:', error);
|
||||||
|
state.messages = [];
|
||||||
|
if (chatMessagesEl) {
|
||||||
|
chatMessagesEl.innerHTML = '';
|
||||||
|
}
|
||||||
|
const errorMessage = (error && error.message && error.message !== 'NETZWERK_FEHLER')
|
||||||
|
? error.message
|
||||||
|
: 'Nachrichten konnten nicht geladen werden. Bitte versuche es erneut.';
|
||||||
|
updateChatState('error', errorMessage);
|
||||||
|
} finally {
|
||||||
|
state.isLoadingMessages = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMessages() {
|
function renderMessages() {
|
||||||
const container = document.getElementById('chatMessages');
|
const container = chatMessagesEl;
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(state.messages) || state.messages.length === 0) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
container.innerHTML = state.messages.map(msg => {
|
container.innerHTML = state.messages.map(msg => {
|
||||||
const isSent = msg.from_user_id === state.currentUserId;
|
const isSent = msg.from_user_id === state.currentUserId;
|
||||||
@@ -2877,12 +3114,17 @@ function renderMessages() {
|
|||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
container.classList.remove('hidden');
|
||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
|
updateChatState(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const input = document.getElementById('chatInput');
|
if (!chatInputEl) {
|
||||||
const message = input.value.trim();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = chatInputEl.value.trim();
|
||||||
|
|
||||||
if (!message || !state.selectedUserId) return;
|
if (!message || !state.selectedUserId) return;
|
||||||
|
|
||||||
@@ -2895,7 +3137,8 @@ async function sendMessage() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
input.value = '';
|
chatInputEl.value = '';
|
||||||
|
chatInputEl.dispatchEvent(new Event('input'));
|
||||||
} else {
|
} else {
|
||||||
alert(result.error);
|
alert(result.error);
|
||||||
}
|
}
|
||||||
@@ -2911,21 +3154,47 @@ async function markAsRead(userId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startSSE() {
|
function startSSE() {
|
||||||
state.eventSource = new EventSource(`?stream=events&last_message_id=${state.lastMessageId}`);
|
if (state.eventSource) {
|
||||||
|
state.eventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `?stream=events&last_message_id=${state.lastMessageId}&t=${Date.now()}`;
|
||||||
|
state.eventSource = new EventSource(url);
|
||||||
|
|
||||||
|
state.eventSource.onopen = () => {
|
||||||
|
state.connectionErrorShown = false;
|
||||||
|
|
||||||
|
if (!state.selectedUserId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isLoadingMessages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.messages.length === 0) {
|
||||||
|
updateChatState('empty', 'Noch keine Nachrichten. Starte das Gespräch!');
|
||||||
|
} else {
|
||||||
|
updateChatState(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
state.eventSource.onmessage = (event) => {
|
state.eventSource.onmessage = (event) => {
|
||||||
|
state.connectionErrorShown = false;
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
if (data.type === 'messages' && data.messages) {
|
if (data.type === 'messages' && data.messages) {
|
||||||
data.messages.forEach(msg => {
|
data.messages.forEach(msg => {
|
||||||
if (msg.id > state.lastMessageId) {
|
const messageId = Number(msg.id);
|
||||||
state.lastMessageId = msg.id;
|
|
||||||
|
if (messageId > state.lastMessageId) {
|
||||||
|
state.lastMessageId = messageId;
|
||||||
|
|
||||||
if (state.selectedUserId &&
|
if (state.selectedUserId &&
|
||||||
((msg.from_user_id === state.selectedUserId && msg.to_user_id === state.currentUserId) ||
|
((msg.from_user_id === state.selectedUserId && msg.to_user_id === state.currentUserId) ||
|
||||||
(msg.from_user_id === state.currentUserId && msg.to_user_id === state.selectedUserId))) {
|
(msg.from_user_id === state.currentUserId && msg.to_user_id === state.selectedUserId))) {
|
||||||
|
|
||||||
if (!state.messages.find(m => m.id === msg.id)) {
|
if (!state.messages.find(m => Number(m.id) === messageId)) {
|
||||||
state.messages.push(msg);
|
state.messages.push(msg);
|
||||||
renderMessages();
|
renderMessages();
|
||||||
|
|
||||||
@@ -2940,6 +3209,22 @@ function startSSE() {
|
|||||||
loadUsers();
|
loadUsers();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state.eventSource.onerror = () => {
|
||||||
|
if (!state.connectionErrorShown) {
|
||||||
|
state.connectionErrorShown = true;
|
||||||
|
console.warn('SSE-Verbindung unterbrochen, versuche Neuverbindung.');
|
||||||
|
if (state.selectedUserId && !state.isLoadingMessages) {
|
||||||
|
updateChatState('error', 'Live-Verbindung unterbrochen. Erneuter Verbindungsversuch…');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.eventSource) {
|
||||||
|
state.eventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(startSSE, 1500);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
@@ -2948,22 +3233,25 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('sendButton').addEventListener('click', sendMessage);
|
sendButtonEl?.addEventListener('click', sendMessage);
|
||||||
document.getElementById('chatInput').addEventListener('keypress', (e) => {
|
|
||||||
|
chatInputEl?.addEventListener('keypress', (e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendMessage();
|
sendMessage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.getElementById('userSearch').addEventListener('input', renderUserList);
|
|
||||||
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
userSearchInput?.addEventListener('input', () => renderUserList());
|
||||||
|
|
||||||
|
document.getElementById('logoutBtn')?.addEventListener('click', async () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('action', 'logout');
|
formData.append('action', 'logout');
|
||||||
await fetch('', { method: 'POST', body: formData });
|
await fetch('', { method: 'POST', body: formData });
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('chatInput').addEventListener('input', function() {
|
chatInputEl?.addEventListener('input', function() {
|
||||||
this.style.height = 'auto';
|
this.style.height = 'auto';
|
||||||
this.style.height = Math.min(this.scrollHeight, 100) + 'px';
|
this.style.height = Math.min(this.scrollHeight, 100) + 'px';
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user