Finished messenger

This commit is contained in:
2026-01-12 10:41:05 +01:00
parent 674fabb715
commit a4d386f2c5
11 changed files with 2069 additions and 426 deletions

View File

@@ -70,6 +70,38 @@ Implementierung einer öffentlichen Benutzerliste, die alle Benutzer und ihre Gr
--- ---
## Notizen-App
#### Persönliche Notizen mit Markdown-Unterstützung
**Beschreibung:**
Implementierung einer vollständigen Notizen-Anwendung mit CRUD-Funktionalität (Create, Read, Update, Delete). Benutzer können persönliche Notizen erstellen, bearbeiten und löschen.
**Features:**
- **Markdown-Unterstützung**: Notizen werden mit SimpleMarkdown gerendert
- **AJAX-Integration**: Alle Operationen ohne Seiten-Neuladung
- **Benutzergebunden**: Jeder Benutzer sieht nur seine eigenen Notizen
- **Echtzeit-Vorschau**: Markdown wird direkt in HTML umgewandelt
**Technische Umsetzung:**
- NoteController mit vollständiger CRUD-Implementierung
- NoteModel für Datenbankoperationen
- SimpleMarkdown-Library für Markdown-Parsing
- AJAX-Endpoints für dynamische Interaktion
**Zugriffsrechte:**
- Nur für angemeldete Benutzer verfügbar
- Jeder Benutzer hat nur Zugriff auf seine eigenen Notizen
```
<!-- Screenshot Platzhalter -->
[📸 Screenshot: Notizen-Übersicht]
[📸 Screenshot: Notiz erstellen/bearbeiten]
[📸 Screenshot: Markdown-Vorschau]
```
---
## jQuery - Einführung und Grundlagen ## jQuery - Einführung und Grundlagen
#### JavaScript Basics #### JavaScript Basics

View File

@@ -63,7 +63,7 @@ return array(
* DB_CHARSET The charset, necessary for security reasons. Check Database.php class for more info. * DB_CHARSET The charset, necessary for security reasons. Check Database.php class for more info.
*/ */
'DB_TYPE' => 'mysql', 'DB_TYPE' => 'mysql',
'DB_HOST' => 'localhost', 'DB_HOST' => '127.0.0.1',
'DB_NAME' => 'huge', 'DB_NAME' => 'huge',
'DB_USER' => 'root', 'DB_USER' => 'root',
'DB_PASS' => 'root', 'DB_PASS' => 'root',

View File

@@ -0,0 +1,144 @@
<?php
/**
* Class DatabaseController
*
* Controller for managing databases and showing their structure
*/
class DatabaseController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// Only logged-in users can access the database manager
Auth::checkAuthentication();
}
/**
* Main database management interface
*/
public function index()
{
$this->View->render('database/index', array(
'databases' => DatabaseModel::getAllDatabases(),
'current_db' => Config::get('DB_NAME')
));
}
/**
* Show details of a specific database
* @param string $database_name
*/
public function show($database_name = null)
{
if (!$database_name) {
$database_name = Config::get('DB_NAME');
}
$this->View->render('database/show', array(
'tables' => DatabaseModel::getTablesInDatabase($database_name),
'database_name' => $database_name,
'table_info' => DatabaseModel::getTableDetails($database_name)
));
}
/**
* Create a new database
*/
public function create()
{
$database_name = Request::post('database_name');
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if (DatabaseModel::createDatabase($database_name)) {
echo json_encode([
'success' => true,
'message' => 'Database created successfully'
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to create database'
]);
}
return;
}
Redirect::to('database');
}
/**
* Delete a database
* @param string $database_name
*/
public function delete($database_name)
{
// Prevent deletion of the current database
if ($database_name === Config::get('DB_NAME')) {
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Cannot delete the currently connected database'
]);
return;
}
Redirect::to('database');
return;
}
$success = DatabaseModel::deleteDatabase($database_name);
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if ($success) {
echo json_encode([
'success' => true,
'message' => 'Database deleted successfully'
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to delete database'
]);
}
return;
}
Redirect::to('database');
}
/**
* Get database structure as JSON (AJAX endpoint)
* @param string $database_name
*/
public function getStructure($database_name = null)
{
if (!$database_name) {
$database_name = Config::get('DB_NAME');
}
$structure = DatabaseModel::getDatabaseStructure($database_name);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'structure' => $structure
]);
}
/**
* Check if the request is an AJAX request
*/
private function isAjaxRequest()
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
}

View File

@@ -0,0 +1,201 @@
<?php
/**
* Class UserController for Database Manager
*
* Controller for managing MySQL users and privileges
*/
class DbUserController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// Only admin users can access database user management
Auth::checkAuthentication();
Auth::checkAdminAuthentication();
}
/**
* List all database users
*/
public function index()
{
$this->View->render('dbuser/index', array(
'users' => DbUserModel::getAllUsers(),
'current_user' => Config::get('DB_USER')
));
}
/**
* Create a new database user
*/
public function create()
{
if (Request::post('submit_create_user')) {
$username = Request::post('username');
$password = Request::post('password');
$host = Request::post('host');
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if (DbUserModel::createUser($username, $password, $host)) {
echo json_encode([
'success' => true,
'message' => 'User created successfully',
'reload' => true
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to create user'
]);
}
return;
}
if (DbUserModel::createUser($username, $password, $host)) {
Redirect::to('dbuser');
} else {
Redirect::to('dbuser');
}
return;
}
// Show create user form
$this->View->render('dbuser/create');
}
/**
* Edit user details and privileges
* @param string $username
* @param string $host
*/
public function edit($username, $host)
{
if (Request::post('submit_edit_user')) {
$new_password = Request::post('password');
$privileges = Request::post('privileges');
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
$success = true;
$message = 'User updated successfully';
if (!empty($new_password)) {
if (!DbUserModel::updateUserPassword($username, $host, $new_password)) {
$success = false;
$message = 'Failed to update user password';
}
}
if ($success && !DbUserModel::updateUserPrivileges($username, $host, $privileges)) {
$success = false;
$message = 'Failed to update user privileges';
}
if ($success) {
echo json_encode([
'success' => true,
'message' => $message
]);
} else {
echo json_encode([
'success' => false,
'message' => $message
]);
}
return;
}
$success = true;
if (!empty($new_password)) {
$success = DbUserModel::updateUserPassword($username, $host, $new_password);
}
if ($success && !DbUserModel::updateUserPrivileges($username, $host, $privileges)) {
$success = false;
}
Redirect::to('dbuser');
return;
}
// Show edit user form
$this->View->render('dbuser/edit', array(
'user' => DbUserModel::getUserDetails($username, $host),
'privileges' => DbUserModel::getUserPrivileges($username, $host),
'databases' => DatabaseModel::getAllDatabases()
));
}
/**
* Delete a user
* @param string $username
* @param string $host
*/
public function delete($username, $host)
{
// Prevent deletion of current user
if ($username === Config::get('DB_USER')) {
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Cannot delete the currently connected user'
]);
return;
}
Redirect::to('dbuser');
return;
}
$success = DbUserModel::deleteUser($username, $host);
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if ($success) {
echo json_encode([
'success' => true,
'message' => 'User deleted successfully'
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to delete user'
]);
}
return;
}
Redirect::to('dbuser');
}
/**
* Show user privileges
* @param string $username
* @param string $host
*/
public function privileges($username, $host)
{
$this->View->render('dbuser/privileges', array(
'user' => DbUserModel::getUserDetails($username, $host),
'privileges' => DbUserModel::getUserPrivileges($username, $host)
));
}
/**
* Check if the request is an AJAX request
*/
private function isAjaxRequest()
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
}

View File

@@ -5,34 +5,23 @@ class MessageController extends Controller
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
// Require login for all message features
Auth::checkAuthentication(); Auth::checkAuthentication();
} }
/**
* Check if the request is an AJAX request
*/
private function isAjaxRequest() private function isAjaxRequest()
{ {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
} }
/**
* Send a message to a specific user via URL parameters
* URL format: message/send/{receiver_id}/{subject}/{message}
*/
public function send() public function send()
{ {
// Handle POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$receiver_id = isset($_POST['receiver_id']) ? $_POST['receiver_id'] : null; $receiver_id = isset($_POST['receiver_id']) ? $_POST['receiver_id'] : null;
$subject = isset($_POST['subject']) ? $_POST['subject'] : 'No Subject'; $subject = isset($_POST['subject']) ? $_POST['subject'] : 'No Subject';
$message = isset($_POST['message']) ? $_POST['message'] : null; $message = isset($_POST['message']) ? $_POST['message'] : null;
if (!$receiver_id || !$message) { if (!$receiver_id || !$message) {
// Return JSON for AJAX requests
if ($this->isAjaxRequest()) { if ($this->isAjaxRequest()) {
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'Receiver and message are required']); echo json_encode(['success' => false, 'message' => 'Receiver and message are required']);
@@ -44,11 +33,9 @@ class MessageController extends Controller
return; return;
} }
// Send the message
$sender_id = Session::get('user_id'); $sender_id = Session::get('user_id');
$success = MessageModel::sendToUser($sender_id, $receiver_id, $subject, $message); $success = MessageModel::sendToUser($sender_id, $receiver_id, $subject, $message);
// Return JSON for AJAX requests
if ($this->isAjaxRequest()) { if ($this->isAjaxRequest()) {
header('Content-Type: application/json'); header('Content-Type: application/json');
if ($success) { if ($success) {
@@ -59,14 +46,12 @@ class MessageController extends Controller
return; return;
} }
// Regular request handling
if ($success) { if ($success) {
Session::add('feedback_positive', 'Message sent successfully'); Session::add('feedback_positive', 'Message sent successfully');
} else { } else {
Session::add('feedback_negative', 'Failed to send message'); Session::add('feedback_negative', 'Failed to send message');
} }
// If coming from conversation view, return there
if (isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'conversation') !== false) { if (isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'conversation') !== false) {
Redirect::to('message/conversation/' . $receiver_id); Redirect::to('message/conversation/' . $receiver_id);
} else { } else {
@@ -75,7 +60,7 @@ class MessageController extends Controller
return; return;
} }
// Handle GET request // GET request: message/send/{receiver_id}/{subject}/{message}
$url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/')); $url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
$receiver_id = isset($url_parts[2]) ? $url_parts[2] : null; $receiver_id = isset($url_parts[2]) ? $url_parts[2] : null;
$subject = isset($url_parts[3]) ? urldecode($url_parts[3]) : null; $subject = isset($url_parts[3]) ? urldecode($url_parts[3]) : null;
@@ -87,7 +72,6 @@ class MessageController extends Controller
return; return;
} }
// Verify receiver exists
$receiver = UserModel::getPublicProfileOfUser($receiver_id); $receiver = UserModel::getPublicProfileOfUser($receiver_id);
if (!$receiver) { if (!$receiver) {
header('Content-Type: application/json'); header('Content-Type: application/json');
@@ -95,7 +79,6 @@ class MessageController extends Controller
return; return;
} }
// Send the message
$sender_id = Session::get('user_id'); $sender_id = Session::get('user_id');
$success = MessageModel::sendToUser($sender_id, $receiver_id, $subject, $message); $success = MessageModel::sendToUser($sender_id, $receiver_id, $subject, $message);
@@ -107,14 +90,8 @@ class MessageController extends Controller
} }
} }
/**
* Send a message to a group via URL parameters
* URL format: message/sendgroup/{group_type}/{subject}/{message}
* group_type can be: admins, moderators, all_users
*/
public function sendgroup() public function sendgroup()
{ {
// Handle POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$group_type = isset($_POST['group_type']) ? $_POST['group_type'] : null; $group_type = isset($_POST['group_type']) ? $_POST['group_type'] : null;
$subject = isset($_POST['subject']) ? $_POST['subject'] : 'No Subject'; $subject = isset($_POST['subject']) ? $_POST['subject'] : 'No Subject';
@@ -126,14 +103,12 @@ class MessageController extends Controller
return; return;
} }
// Validate group type
if (!in_array($group_type, ['admins', 'moderators', 'all_users'])) { if (!in_array($group_type, ['admins', 'moderators', 'all_users'])) {
Session::add('feedback_negative', 'Invalid group type'); Session::add('feedback_negative', 'Invalid group type');
Redirect::to('message'); Redirect::to('message');
return; return;
} }
// Send the message
$sender_id = Session::get('user_id'); $sender_id = Session::get('user_id');
$success = MessageModel::sendToGroup($sender_id, $group_type, $subject, $message); $success = MessageModel::sendToGroup($sender_id, $group_type, $subject, $message);
@@ -147,7 +122,7 @@ class MessageController extends Controller
return; return;
} }
// Handle GET request // GET request: message/sendgroup/{group_type}/{subject}/{message}
$url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/')); $url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
$group_type = isset($url_parts[2]) ? $url_parts[2] : null; $group_type = isset($url_parts[2]) ? $url_parts[2] : null;
$subject = isset($url_parts[3]) ? urldecode($url_parts[3]) : null; $subject = isset($url_parts[3]) ? urldecode($url_parts[3]) : null;
@@ -159,14 +134,12 @@ class MessageController extends Controller
return; return;
} }
// Validate group type
if (!in_array($group_type, ['admins', 'moderators', 'all_users'])) { if (!in_array($group_type, ['admins', 'moderators', 'all_users'])) {
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'Invalid group type. Must be: admins, moderators, or all_users']); echo json_encode(['success' => false, 'message' => 'Invalid group type. Must be: admins, moderators, or all_users']);
return; return;
} }
// Send the message
$sender_id = Session::get('user_id'); $sender_id = Session::get('user_id');
$success = MessageModel::sendToGroup($sender_id, $group_type, $subject, $message); $success = MessageModel::sendToGroup($sender_id, $group_type, $subject, $message);
@@ -178,18 +151,12 @@ class MessageController extends Controller
} }
} }
/**
* Handle reply to a message
*/
public function reply() public function reply()
{ {
// Always return JSON for this endpoint while (ob_get_level()) ob_end_clean();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Start output buffering to catch any accidental output
ob_start();
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Invalid request method']); echo json_encode(['success' => false, 'message' => 'Invalid request method']);
exit(); exit();
@@ -209,41 +176,25 @@ class MessageController extends Controller
exit(); exit();
} }
// Send the message (using sendToUser without subject) $success = MessageModel::sendToUser($sender_id, $receiver_id, 'Direct Message', $message);
$success = MessageModel::sendToUser($sender_id, $receiver_id, 'Re: Message', $message);
if ($success) { if ($success) {
echo json_encode(['success' => true, 'message' => 'Reply sent successfully']); echo json_encode(['success' => true, 'message' => 'Reply sent successfully']);
} else { } else {
echo json_encode(['success' => false, 'message' => 'Failed to send reply']); echo json_encode(['success' => false, 'message' => 'Failed to send reply']);
} }
} catch (Exception $e) {
// Catch any PHP errors
echo json_encode(['success' => false, 'message' => 'Server error: ' . $e->getMessage()]);
}
// Clean any output buffer and exit
ob_end_clean();
exit(); exit();
} }
/**
* Show global chat interface
*/
public function global() public function global()
{ {
// Redirect to main messages page with global chat hash
Redirect::to('message#load-global'); Redirect::to('message#load-global');
} }
/**
* Show the messenger interface
*/
public function index() public function index()
{ {
$user_id = Session::get('user_id'); $user_id = Session::get('user_id');
// Get conversations and unread count
$conversations = MessageModel::getConversations($user_id); $conversations = MessageModel::getConversations($user_id);
$unread_count = MessageModel::getUnreadCount($user_id); $unread_count = MessageModel::getUnreadCount($user_id);
@@ -254,9 +205,6 @@ class MessageController extends Controller
)); ));
} }
/**
* Show conversation with a specific user
*/
public function conversation() public function conversation()
{ {
$user_id = Session::get('user_id'); $user_id = Session::get('user_id');
@@ -268,69 +216,53 @@ class MessageController extends Controller
return; return;
} }
// Get user info for the other person
$other_user = UserModel::getPublicProfileOfUser($other_user_id); $other_user = UserModel::getPublicProfileOfUser($other_user_id);
if (!$other_user) { if (!$other_user) {
Redirect::to('message'); Redirect::to('message');
return; return;
} }
// Redirect to main messages page with conversation hash
Redirect::to('message#load-conversation-' . $other_user_id); Redirect::to('message#load-conversation-' . $other_user_id);
} }
/**
* Get conversation messages as JSON (AJAX endpoint)
*/
public function getConversationMessages() public function getConversationMessages()
{ {
while (ob_get_level()) ob_end_clean();
$user_id = Session::get('user_id'); $user_id = Session::get('user_id');
$url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/')); $url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
$other_user_id = isset($url_parts[2]) ? $url_parts[2] : null; $other_user_id = isset($url_parts[2]) ? $url_parts[2] : null;
header('Content-Type: application/json');
if (!$other_user_id) { if (!$other_user_id) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'Missing user ID']); echo json_encode(['success' => false, 'message' => 'Missing user ID']);
return;
}
// Get messages
$messages = MessageModel::getMessagesWithUser($user_id, $other_user_id);
// Mark messages as read when loading the conversation
MessageModel::markAsRead($user_id, $other_user_id);
header('Content-Type: application/json');
echo json_encode(['success' => true, 'messages' => $messages]);
}
/**
* Get global chat messages as JSON (AJAX endpoint)
*/
public function getGlobalMessages()
{
// Always return JSON for this endpoint
header('Content-Type: application/json');
$messages = MessageModel::getGlobalMessages();
echo json_encode(['success' => true, 'messages' => $messages]);
// Stop any further execution
exit(); exit();
} }
/** $messages = MessageModel::getMessagesWithUser($user_id, $other_user_id);
* Send message to global chat MessageModel::markAsRead($user_id, $other_user_id);
*/
echo json_encode(['success' => true, 'messages' => $messages]);
exit();
}
public function getGlobalMessages()
{
while (ob_get_level()) ob_end_clean();
header('Content-Type: application/json');
$messages = MessageModel::getGlobalMessages();
echo json_encode(['success' => true, 'messages' => $messages]);
exit();
}
public function sendToGlobal() public function sendToGlobal()
{ {
// Always return JSON for this endpoint while (ob_get_level()) ob_end_clean();
header('Content-Type: application/json'); header('Content-Type: application/json');
// Start output buffering to catch any accidental output
ob_start();
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Invalid request method']); echo json_encode(['success' => false, 'message' => 'Invalid request method']);
exit(); exit();
@@ -356,25 +288,18 @@ class MessageController extends Controller
} else { } else {
echo json_encode(['success' => false, 'message' => 'Failed to send message']); echo json_encode(['success' => false, 'message' => 'Failed to send message']);
} }
} catch (Exception $e) {
// Catch any PHP errors
echo json_encode(['success' => false, 'message' => 'Server error: ' . $e->getMessage()]);
}
// Clean any output buffer and exit
ob_end_clean();
exit(); exit();
} }
/**
* Get unread count as JSON
*/
public function unreadcount() public function unreadcount()
{ {
while (ob_get_level()) ob_end_clean();
$user_id = Session::get('user_id'); $user_id = Session::get('user_id');
$unread_count = MessageModel::getUnreadCount($user_id); $unread_count = MessageModel::getUnreadCount($user_id);
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode(['unread_count' => $unread_count]); echo json_encode(['unread_count' => $unread_count]);
exit();
} }
} }

View File

@@ -0,0 +1,181 @@
<?php
/**
* Class SqlController
*
* Controller for executing raw SQL queries
*/
class SqlController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// Only logged-in users can access the SQL console
Auth::checkAuthentication();
}
/**
* Show SQL console interface
* @param string $database_name
*/
public function index($database_name = null)
{
if (!$database_name) {
$database_name = Config::get('DB_NAME');
}
$this->View->render('sql/index', array(
'database_name' => $database_name,
'databases' => DatabaseModel::getAllDatabases(),
'history' => SqlModel::getQueryHistory(Session::get('user_id'))
));
}
/**
* Execute SQL query
*/
public function execute()
{
$database_name = Request::post('database_name') ?: Config::get('DB_NAME');
$sql_query = Request::post('sql_query');
if (empty($sql_query)) {
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'SQL query cannot be empty'
]);
return;
}
Redirect::to('sql');
return;
}
$result = SqlModel::executeQuery($database_name, $sql_query, Session::get('user_id'));
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if ($result['success']) {
echo json_encode([
'success' => true,
'message' => $result['message'],
'result' => $result['result'],
'affected_rows' => $result['affected_rows'],
'execution_time' => $result['execution_time'],
'query_type' => $result['query_type']
]);
} else {
echo json_encode([
'success' => false,
'message' => $result['message'],
'error' => $result['error']
]);
}
return;
}
// Non-AJAX: redirect with results in session
Session::set('sql_result', $result);
Redirect::to('sql/index/' . urlencode($database_name));
}
/**
* Get query history as JSON (AJAX endpoint)
*/
public function getHistory()
{
$history = SqlModel::getQueryHistory(Session::get('user_id'));
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'history' => $history
]);
}
/**
* Clear query history
*/
public function clearHistory()
{
$success = SqlModel::clearQueryHistory(Session::get('user_id'));
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if ($success) {
echo json_encode([
'success' => true,
'message' => 'Query history cleared successfully'
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to clear query history'
]);
}
return;
}
Redirect::to('sql');
}
/**
* Get database schema for autocomplete
* @param string $database_name
*/
public function getSchema($database_name = null)
{
if (!$database_name) {
$database_name = Config::get('DB_NAME');
}
$schema = SqlModel::getDatabaseSchema($database_name);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'schema' => $schema
]);
}
/**
* Format SQL query (AJAX endpoint)
*/
public function formatQuery()
{
$query = Request::post('query');
if (empty($query)) {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Query cannot be empty'
]);
return;
}
$formatted = SqlModel::formatQuery($query);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'formatted' => $formatted
]);
}
/**
* Check if the request is an AJAX request
*/
private function isAjaxRequest()
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
}

View File

@@ -0,0 +1,248 @@
<?php
/**
* Class TableController
*
* Controller for managing database tables
*/
class TableController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// Only logged-in users can access the table manager
Auth::checkAuthentication();
}
/**
* Show table content with pagination
* @param string $database_name
* @param string $table_name
* @param int $page
*/
public function show($database_name = null, $table_name = null, $page = 1)
{
if (!$database_name) {
$database_name = Config::get('DB_NAME');
}
if (!$table_name) {
Redirect::to('database/show/' . urlencode($database_name));
return;
}
$page = (int)$page;
$per_page = 20;
$this->View->render('table/show', array(
'database_name' => $database_name,
'table_name' => $table_name,
'columns' => TableModel::getTableColumns($database_name, $table_name),
'rows' => TableModel::getTableRows($database_name, $table_name, $page, $per_page),
'total_rows' => TableModel::getTableRowCount($database_name, $table_name),
'current_page' => $page,
'per_page' => $per_page,
'table_info' => TableModel::getTableInfo($database_name, $table_name)
));
}
/**
* Create a new table
* @param string $database_name
*/
public function create($database_name = null)
{
if (!$database_name) {
$database_name = Config::get('DB_NAME');
}
if (Request::post('submit_create_table')) {
$table_name = Request::post('table_name');
$columns = Request::post('columns');
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if (TableModel::createTable($database_name, $table_name, $columns)) {
echo json_encode([
'success' => true,
'message' => 'Table created successfully',
'redirect' => Config::get('URL') . 'table/show/' . urlencode($database_name) . '/' . urlencode($table_name)
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to create table'
]);
}
return;
}
if (TableModel::createTable($database_name, $table_name, $columns)) {
Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name));
} else {
Redirect::to('database/show/' . urlencode($database_name));
}
return;
}
// Show create table form
$this->View->render('table/create', array(
'database_name' => $database_name
));
}
/**
* Show table structure
* @param string $database_name
* @param string $table_name
*/
public function structure($database_name = null, $table_name = null)
{
if (!$database_name) {
$database_name = Config::get('DB_NAME');
}
if (!$table_name) {
Redirect::to('database/show/' . urlencode($database_name));
return;
}
$this->View->render('table/structure', array(
'database_name' => $database_name,
'table_name' => $table_name,
'columns' => TableModel::getTableColumns($database_name, $table_name),
'indexes' => TableModel::getTableIndexes($database_name, $table_name),
'table_info' => TableModel::getTableInfo($database_name, $table_name)
));
}
/**
* Add a column to a table
* @param string $database_name
* @param string $table_name
*/
public function addColumn($database_name = null, $table_name = null)
{
if (!$database_name) {
$database_name = Config::get('DB_NAME');
}
if (!$table_name) {
Redirect::to('database/show/' . urlencode($database_name));
return;
}
if (Request::post('submit_add_column')) {
$column_name = Request::post('column_name');
$column_type = Request::post('column_type');
$column_null = Request::post('column_null');
$column_key = Request::post('column_key');
$column_default = Request::post('column_default');
$column_extra = Request::post('column_extra');
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if (TableModel::addColumn($database_name, $table_name, $column_name, $column_type, $column_null, $column_key, $column_default, $column_extra)) {
echo json_encode([
'success' => true,
'message' => 'Column added successfully',
'reload' => true
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to add column'
]);
}
return;
}
if (TableModel::addColumn($database_name, $table_name, $column_name, $column_type, $column_null, $column_key, $column_default, $column_extra)) {
Redirect::to('table/structure/' . urlencode($database_name) . '/' . urlencode($table_name));
} else {
Redirect::to('table/structure/' . urlencode($database_name) . '/' . urlencode($table_name));
}
return;
}
// Show add column form
$this->View->render('table/add_column', array(
'database_name' => $database_name,
'table_name' => $table_name
));
}
/**
* Drop a column from a table
* @param string $database_name
* @param string $table_name
* @param string $column_name
*/
public function dropColumn($database_name, $table_name, $column_name)
{
$success = TableModel::dropColumn($database_name, $table_name, $column_name);
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if ($success) {
echo json_encode([
'success' => true,
'message' => 'Column dropped successfully',
'reload' => true
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to drop column'
]);
}
return;
}
Redirect::to('table/structure/' . urlencode($database_name) . '/' . urlencode($table_name));
}
/**
* Delete a table
* @param string $database_name
* @param string $table_name
*/
public function delete($database_name, $table_name)
{
$success = TableModel::deleteTable($database_name, $table_name);
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if ($success) {
echo json_encode([
'success' => true,
'message' => 'Table deleted successfully',
'redirect' => Config::get('URL') . 'database/show/' . urlencode($database_name)
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to delete table'
]);
}
return;
}
Redirect::to('database/show/' . urlencode($database_name));
}
/**
* Check if the request is an AJAX request
*/
private function isAjaxRequest()
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
}

View File

@@ -0,0 +1,183 @@
<?php
/**
* Class DatabaseModel
*
* Model for database operations using PDO
*/
class DatabaseModel
{
/**
* Get all databases on the server
* @return array
*/
public static function getAllDatabases()
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SHOW DATABASES";
$query = $database->prepare($sql);
$query->execute();
$databases = $query->fetchAll(PDO::FETCH_COLUMN);
// Filter out system databases
$system_dbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
return array_diff($databases, $system_dbs);
}
/**
* Get all tables in a specific database
* @param string $database_name
* @return array
*/
public static function getTablesInDatabase($database_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SHOW TABLES FROM " . $database_name;
$query = $database->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_COLUMN);
}
/**
* Get detailed information about tables in a database
* @param string $database_name
* @return array
*/
public static function getTableDetails($database_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$tables = self::getTablesInDatabase($database_name);
$table_details = array();
foreach ($tables as $table) {
$sql = "SHOW TABLE STATUS FROM " . $database_name . " LIKE :table_name";
$query = $database->prepare($sql);
$query->execute(array(':table_name' => $table));
$details = $query->fetch(PDO::FETCH_ASSOC);
if ($details) {
$table_details[$table] = array(
'engine' => $details['Engine'],
'rows' => $details['Rows'],
'data_size' => self::formatBytes($details['Data_length']),
'index_size' => self::formatBytes($details['Index_length']),
'total_size' => self::formatBytes($details['Data_length'] + $details['Index_length']),
'collation' => $details['Collation'],
'comment' => $details['Comment']
);
}
}
return $table_details;
}
/**
* Get complete database structure (tables and columns)
* @param string $database_name
* @return array
*/
public static function getDatabaseStructure($database_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$structure = array();
$tables = self::getTablesInDatabase($database_name);
foreach ($tables as $table) {
$sql = "DESCRIBE " . $database_name . "." . $table;
$query = $database->prepare($sql);
$query->execute();
$columns = $query->fetchAll(PDO::FETCH_ASSOC);
$structure[$table] = $columns;
}
return $structure;
}
/**
* Create a new database
* @param string $database_name
* @return bool
*/
public static function createDatabase($database_name)
{
if (!$database_name || !preg_match('/^[a-zA-Z0-9_]+$/', $database_name)) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
try {
$sql = "CREATE DATABASE `" . $database_name . "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
$query = $database->prepare($sql);
return $query->execute();
} catch (PDOException $e) {
return false;
}
}
/**
* Delete a database
* @param string $database_name
* @return bool
*/
public static function deleteDatabase($database_name)
{
if (!$database_name) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
try {
$sql = "DROP DATABASE `" . $database_name . "`";
$query = $database->prepare($sql);
return $query->execute();
} catch (PDOException $e) {
return false;
}
}
/**
* Get table columns with details
* @param string $database_name
* @param string $table_name
* @return array
*/
public static function getTableColumns($database_name, $table_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SHOW COLUMNS FROM " . $database_name . "." . $table_name;
$query = $database->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Format bytes to human readable format
* @param int $bytes
* @return string
*/
private static function formatBytes($bytes)
{
if ($bytes >= 1073741824) {
return number_format($bytes / 1073741824, 2) . ' GB';
} elseif ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
} elseif ($bytes > 1) {
return $bytes . ' bytes';
} elseif ($bytes == 1) {
return '1 byte';
} else {
return '0 bytes';
}
}
}

View File

@@ -0,0 +1,289 @@
<?php
/**
* Class SqlModel
*
* Model for executing raw SQL queries
*/
class SqlModel
{
/**
* Execute a SQL query and return the result
* @param string $database_name
* @param string $sql_query
* @param int $user_id
* @return array
*/
public static function executeQuery($database_name, $sql_query, $user_id = null)
{
$start_time = microtime(true);
// Save query to history if user_id is provided
if ($user_id) {
self::saveQueryToHistory($user_id, $database_name, $sql_query);
}
try {
$database = DatabaseFactory::getFactory()->getConnection();
// Determine query type
$query_type = self::getQueryType($sql_query);
// Execute the query
$query = $database->prepare($sql_query);
$query->execute();
$execution_time = number_format((microtime(true) - $start_time) * 1000, 2);
// Handle different query types
if ($query_type === 'SELECT' || $query_type === 'SHOW' || $query_type === 'DESCRIBE' || $query_type === 'EXPLAIN') {
// Return result set
$result = $query->fetchAll(PDO::FETCH_ASSOC);
$message = 'Query executed successfully. ' . count($result) . ' rows returned.';
return array(
'success' => true,
'message' => $message,
'result' => $result,
'query_type' => $query_type,
'execution_time' => $execution_time,
'affected_rows' => 0
);
} else {
// Return affected rows count
$affected_rows = $query->rowCount();
$message = 'Query executed successfully. ' . $affected_rows . ' row(s) affected.';
return array(
'success' => true,
'message' => $message,
'query_type' => $query_type,
'execution_time' => $execution_time,
'affected_rows' => $affected_rows,
'result' => array()
);
}
} catch (PDOException $e) {
return array(
'success' => false,
'message' => 'Query execution failed',
'error' => $e->getMessage(),
'query_type' => 'ERROR',
'execution_time' => 0,
'result' => array()
);
}
}
/**
* Save query to history
* @param int $user_id
* @param string $database_name
* @param string $sql_query
* @return bool
*/
private static function saveQueryToHistory($user_id, $database_name, $sql_query)
{
// Create history table if it doesn't exist
self::createHistoryTableIfNotExists();
$database = DatabaseFactory::getFactory()->getConnection();
try {
$sql = "INSERT INTO sql_query_history (user_id, database_name, query_text, query_timestamp)
VALUES (:user_id, :database_name, :query_text, NOW())";
$query = $database->prepare($sql);
$query->execute(array(
':user_id' => $user_id,
':database_name' => $database_name,
':query_text' => $sql_query
));
// Keep only last 50 queries per user
$sql = "DELETE FROM sql_query_history
WHERE user_id = :user_id
AND id NOT IN (
SELECT id FROM (
SELECT id FROM sql_query_history
WHERE user_id = :user_id
ORDER BY query_timestamp DESC
LIMIT 50
) AS temp
)";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => $user_id));
return true;
} catch (PDOException $e) {
return false;
}
}
/**
* Create SQL query history table if it doesn't exist
*/
private static function createHistoryTableIfNotExists()
{
$database = DatabaseFactory::getFactory()->getConnection();
try {
$sql = "CREATE TABLE IF NOT EXISTS sql_query_history (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
database_name VARCHAR(64) NOT NULL,
query_text TEXT NOT NULL,
query_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_timestamp (user_id, query_timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
$database->exec($sql);
} catch (PDOException $e) {
// Table creation failed
}
}
/**
* Get query history for a user
* @param int $user_id
* @param int $limit
* @return array
*/
public static function getQueryHistory($user_id, $limit = 50)
{
$database = DatabaseFactory::getFactory()->getConnection();
try {
$sql = "SELECT database_name, query_text, query_timestamp
FROM sql_query_history
WHERE user_id = :user_id
ORDER BY query_timestamp DESC
LIMIT :limit";
$query = $database->prepare($sql);
$query->bindValue(':user_id', $user_id, PDO::PARAM_INT);
$query->bindValue(':limit', $limit, PDO::PARAM_INT);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
return array();
}
}
/**
* Clear query history for a user
* @param int $user_id
* @return bool
*/
public static function clearQueryHistory($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
try {
$sql = "DELETE FROM sql_query_history WHERE user_id = :user_id";
$query = $database->prepare($sql);
return $query->execute(array(':user_id' => $user_id));
} catch (PDOException $e) {
return false;
}
}
/**
* Get database schema for autocomplete
* @param string $database_name
* @return array
*/
public static function getDatabaseSchema($database_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$schema = array();
try {
// Get all tables
$sql = "SHOW TABLES FROM " . $database_name;
$query = $database->prepare($sql);
$query->execute();
$tables = $query->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
// Get columns for each table
$sql = "SHOW COLUMNS FROM " . $database_name . "." . $table;
$query = $database->prepare($sql);
$query->execute();
$columns = $query->fetchAll(PDO::FETCH_ASSOC);
$schema[$table] = array_map(function($column) {
return $column['Field'];
}, $columns);
}
return $schema;
} catch (PDOException $e) {
return array();
}
}
/**
* Format SQL query (basic formatting)
* @param string $query
* @return string
*/
public static function formatQuery($query)
{
// Basic SQL formatting
$query = trim($query);
// Uppercase SQL keywords
$keywords = array(
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'ORDER BY', 'GROUP BY',
'HAVING', 'LIMIT', 'OFFSET', 'INSERT', 'INTO', 'VALUES',
'UPDATE', 'SET', 'DELETE', 'CREATE', 'TABLE', 'ALTER', 'DROP',
'INDEX', 'PRIMARY KEY', 'FOREIGN KEY', 'REFERENCES', 'JOIN',
'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'ON', 'AS', 'DISTINCT',
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'EXISTS', 'IN', 'BETWEEN',
'LIKE', 'IS NULL', 'IS NOT NULL', 'UNION', 'ALL'
);
foreach ($keywords as $keyword) {
$query = preg_replace('/\b' . preg_quote($keyword) . '\b/i', $keyword, $query);
}
// Add line breaks for better readability
$query = preg_replace('/\b(FROM|WHERE|GROUP BY|ORDER BY|HAVING|LIMIT|UNION)\b/', "\n$1", $query);
$query = preg_replace('/,\s*(\w)/', ",\n $1", $query);
return $query;
}
/**
* Determine the type of SQL query
* @param string $sql_query
* @return string
*/
private static function getQueryType($sql_query)
{
$query = strtoupper(trim($sql_query));
if (strpos($query, 'SELECT') === 0) {
return 'SELECT';
} elseif (strpos($query, 'INSERT') === 0) {
return 'INSERT';
} elseif (strpos($query, 'UPDATE') === 0) {
return 'UPDATE';
} elseif (strpos($query, 'DELETE') === 0) {
return 'DELETE';
} elseif (strpos($query, 'CREATE') === 0) {
return 'CREATE';
} elseif (strpos($query, 'ALTER') === 0) {
return 'ALTER';
} elseif (strpos($query, 'DROP') === 0) {
return 'DROP';
} elseif (strpos($query, 'SHOW') === 0) {
return 'SHOW';
} elseif (strpos($query, 'DESCRIBE') === 0) {
return 'DESCRIBE';
} elseif (strpos($query, 'EXPLAIN') === 0) {
return 'EXPLAIN';
} else {
return 'OTHER';
}
}
}

View File

@@ -0,0 +1,284 @@
<?php
/**
* Class TableModel
*
* Model for table operations using PDO
*/
class TableModel
{
/**
* Get table rows with pagination
* @param string $database_name
* @param string $table_name
* @param int $page
* @param int $per_page
* @return array
*/
public static function getTableRows($database_name, $table_name, $page = 1, $per_page = 20)
{
$database = DatabaseFactory::getFactory()->getConnection();
$offset = ($page - 1) * $per_page;
$sql = "SELECT * FROM `" . $database_name . "`.`" . $table_name . "` LIMIT :offset, :per_page";
$query = $database->prepare($sql);
$query->bindParam(':offset', $offset, PDO::PARAM_INT);
$query->bindParam(':per_page', $per_page, PDO::PARAM_INT);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Get total number of rows in a table
* @param string $database_name
* @param string $table_name
* @return int
*/
public static function getTableRowCount($database_name, $table_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT COUNT(*) as count FROM `" . $database_name . "`.`" . $table_name . "`";
$query = $database->prepare($sql);
$query->execute();
$result = $query->fetch(PDO::FETCH_ASSOC);
return (int)$result['count'];
}
/**
* Get table column information
* @param string $database_name
* @param string $table_name
* @return array
*/
public static function getTableColumns($database_name, $table_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SHOW COLUMNS FROM `" . $database_name . "`.`" . $table_name . "`";
$query = $database->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Get table indexes
* @param string $database_name
* @param string $table_name
* @return array
*/
public static function getTableIndexes($database_name, $table_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SHOW INDEX FROM `" . $database_name . "`.`" . $table_name . "`";
$query = $database->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Get table information
* @param string $database_name
* @param string $table_name
* @return array
*/
public static function getTableInfo($database_name, $table_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SHOW TABLE STATUS FROM `" . $database_name . "` LIKE :table_name";
$query = $database->prepare($sql);
$query->execute(array(':table_name' => $table_name));
$result = $query->fetch(PDO::FETCH_ASSOC);
if ($result) {
return array(
'engine' => $result['Engine'],
'rows' => $result['Rows'],
'data_size' => self::formatBytes($result['Data_length']),
'index_size' => self::formatBytes($result['Index_length']),
'total_size' => self::formatBytes($result['Data_length'] + $result['Index_length']),
'collation' => $result['Collation'],
'comment' => $result['Comment'],
'auto_increment' => $result['Auto_increment'],
'create_time' => $result['Create_time'],
'update_time' => $result['Update_time']
);
}
return array();
}
/**
* Create a new table
* @param string $database_name
* @param string $table_name
* @param array $columns
* @return bool
*/
public static function createTable($database_name, $table_name, $columns)
{
if (!$database_name || !$table_name || empty($columns)) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
// Build column definitions
$column_definitions = array();
foreach ($columns as $column) {
$definition = "`" . $column['name'] . "` " . $column['type'];
if (isset($column['null']) && $column['null'] === 'NO') {
$definition .= " NOT NULL";
}
if (isset($column['default']) && $column['default'] !== '') {
$definition .= " DEFAULT '" . $column['default'] . "'";
}
if (isset($column['extra']) && $column['extra'] === 'auto_increment') {
$definition .= " AUTO_INCREMENT";
}
$column_definitions[] = $definition;
}
// Handle primary key
foreach ($columns as $column) {
if (isset($column['key']) && $column['key'] === 'PRI') {
$column_definitions[] = "PRIMARY KEY (`" . $column['name'] . "`)";
break;
}
}
$columns_sql = implode(', ', $column_definitions);
try {
$sql = "CREATE TABLE `" . $database_name . "`.`" . $table_name . "` (" . $columns_sql . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
$query = $database->prepare($sql);
return $query->execute();
} catch (PDOException $e) {
return false;
}
}
/**
* Delete a table
* @param string $database_name
* @param string $table_name
* @return bool
*/
public static function deleteTable($database_name, $table_name)
{
if (!$database_name || !$table_name) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
try {
$sql = "DROP TABLE `" . $database_name . "`.`" . $table_name . "`";
$query = $database->prepare($sql);
return $query->execute();
} catch (PDOException $e) {
return false;
}
}
/**
* Add a column to a table
* @param string $database_name
* @param string $table_name
* @param string $column_name
* @param string $column_type
* @param string $column_null
* @param string $column_key
* @param string $column_default
* @param string $column_extra
* @return bool
*/
public static function addColumn($database_name, $table_name, $column_name, $column_type, $column_null, $column_key, $column_default, $column_extra)
{
if (!$database_name || !$table_name || !$column_name || !$column_type) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$definition = "`" . $column_name . "` " . $column_type;
if ($column_null === 'NO') {
$definition .= " NOT NULL";
}
if ($column_default !== '') {
$definition .= " DEFAULT '" . $column_default . "'";
}
if ($column_extra === 'auto_increment') {
$definition .= " AUTO_INCREMENT";
}
try {
$sql = "ALTER TABLE `" . $database_name . "`.`" . $table_name . "` ADD " . $definition;
$query = $database->prepare($sql);
return $query->execute();
} catch (PDOException $e) {
return false;
}
}
/**
* Drop a column from a table
* @param string $database_name
* @param string $table_name
* @param string $column_name
* @return bool
*/
public static function dropColumn($database_name, $table_name, $column_name)
{
if (!$database_name || !$table_name || !$column_name) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
try {
$sql = "ALTER TABLE `" . $database_name . "`.`" . $table_name . "` DROP COLUMN `" . $column_name . "`";
$query = $database->prepare($sql);
return $query->execute();
} catch (PDOException $e) {
return false;
}
}
/**
* Format bytes to human readable format
* @param int $bytes
* @return string
*/
private static function formatBytes($bytes)
{
if ($bytes >= 1073741824) {
return number_format($bytes / 1073741824, 2) . ' GB';
} elseif ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
} elseif ($bytes > 1) {
return $bytes . ' bytes';
} elseif ($bytes == 1) {
return '1 byte';
} else {
return '0 bytes';
}
}
}

View File

@@ -2,98 +2,82 @@
<h1>Messenger</h1> <h1>Messenger</h1>
<?php $this->renderFeedbackMessages(); ?> <?php $this->renderFeedbackMessages(); ?>
<!-- Add user ID for JavaScript -->
<meta name="user-id" content="<?= Session::get('user_id') ?>"> <meta name="user-id" content="<?= Session::get('user_id') ?>">
<script src="<?= Config::get('URL') ?>public/js/messaging.js"></script> <script src="<?= Config::get('URL') ?>public/js/messaging.js"></script>
<div style="display: flex; gap: 20px; margin-top: 20px;"> <div class="messenger-container">
<!-- Left Sidebar -->
<div style="width: 300px;" class="messaging-sidebar"> <div style="width: 300px;" class="messaging-sidebar">
<div style="margin-bottom: 20px; padding: 15px; background: #f5f5f5; border: 1px solid #ddd;"> <div class="sidebar-section">
<h3 style="margin: 0 0 15px 0;">Channels</h3> <h3>Channels</h3>
<div onclick="loadGlobalChat()" style="display: block; padding: 10px; margin-bottom: 8px; background: white; border: 1px solid #ddd; text-decoration: none; color: #333; cursor: pointer; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#f0f0f0'" onmouseout="this.style.backgroundColor='white'" id="global-chat-link"> <div onclick="loadGlobalChat()" class="chat-item" id="global-chat-link">
<div style="display: flex; align-items: center; gap: 8px;"> <div class="chat-item-title">
<span style="color: #2196F3;">#</span> <span class="chat-item-icon" style="color: #2196F3;">#</span>
<strong>Global Chat</strong> <strong>Global Chat</strong>
</div> </div>
<small style="color: #666; font-size: 11px;">Public chatroom</small> <div class="chat-item-meta">Public chatroom</div>
</div> </div>
</div> </div>
<div style="margin-bottom: 20px; padding: 15px; background: #f5f5f5; border: 1px solid #ddd;"> <div class="sidebar-section">
<h3 style="margin: 0 0 15px 0;">Direct Messages</h3> <h3>Direct Messages</h3>
<?php if (empty($this->conversations)): ?> <?php if (empty($this->conversations)): ?>
<p style="text-align: center; padding: 20px; background: white; border: 1px solid #ddd;">No conversations yet</p> <p class="chat-item" style="text-align: center; cursor: default;">No conversations yet</p>
<?php else: ?> <?php else: ?>
<?php foreach ($this->conversations as $conv): ?> <?php foreach ($this->conversations as $conv): ?>
<div onclick="loadConversation(<?= $conv->other_user_id ?>, '<?= htmlspecialchars(addslashes($conv->user_name)) ?>')" <div onclick="loadConversation(<?= $conv->other_user_id ?>, '<?= htmlspecialchars(addslashes($conv->user_name)) ?>')"
style="display: block; padding: 10px; margin-bottom: 8px; background: white; border: 1px solid #ddd; text-decoration: none; color: #333; cursor: pointer; transition: background-color 0.2s;" class="chat-item"
onmouseover="this.style.backgroundColor='#f0f0f0'"
onmouseout="this.style.backgroundColor='white'"
id="conversation-<?= $conv->other_user_id ?>"> id="conversation-<?= $conv->other_user_id ?>">
<div style="display: flex; justify-content: space-between; align-items: center;"> <div class="chat-item-header">
<div style="display: flex; align-items: center; gap: 8px;"> <div class="chat-item-title">
<span style="color: #4CAF50;">@</span> <span class="chat-item-icon" style="color: #4CAF50;">@</span>
<strong><?= htmlspecialchars($conv->user_name) ?></strong> <strong><?= htmlspecialchars($conv->user_name) ?></strong>
</div> </div>
<?php if ($conv->unread_count > 0): ?> <?php if ($conv->unread_count > 0): ?>
<span style="background: #f44336; color: white; padding: 2px 6px; border-radius: 10px; font-size: 11px;"> <span class="unread-badge"><?= $conv->unread_count ?></span>
<?= $conv->unread_count ?>
</span>
<?php endif; ?> <?php endif; ?>
</div> </div>
<small style="color: #666; font-size: 11px;"><?= date('M j, H:i', strtotime($conv->last_message_time)) ?></small> <div class="chat-item-meta"><?= date('M j, H:i', strtotime($conv->last_message_time)) ?></div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div style="padding: 15px; background: #f5f5f5; border: 1px solid #ddd;"> <div class="sidebar-section">
<h3 style="margin: 0 0 15px 0;">New Message</h3> <h3>New Message</h3>
<form action="<?= Config::get('URL') ?>message/send" method="post" id="message-form"> <form action="<?= Config::get('URL') ?>message/send" method="post" id="message-form" class="new-message-form">
<select name="receiver_id" style="width: 100%; padding: 8px; margin-bottom: 8px; border: 1px solid #ddd;" required> <select name="receiver_id" required>
<option value="">Select user</option> <option value="">Select user</option>
<?php foreach ($this->all_users as $user): ?> <?php foreach ($this->all_users as $user): ?>
<option value="<?= $user->user_id ?>"><?= htmlspecialchars($user->user_name) ?></option> <option value="<?= $user->user_id ?>"><?= htmlspecialchars($user->user_name) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<input type="text" name="subject" placeholder="Subject" style="width: 100%; padding: 8px; margin-bottom: 8px; border: 1px solid #ddd; max-width: 250px;" required> <input type="hidden" name="subject" value="Direct Message">
<textarea name="message" placeholder="Message" style="width: 100%; padding: 8px; margin-bottom: 8px; border: 1px solid #ddd; height: 60px; resize: vertical; max-width: 250px;" required></textarea> <textarea name="message" placeholder="Type your message..." required></textarea>
<button type="submit" class="button">Send Message</button> <button type="submit" class="button" style="width: 100%;">Send Message</button>
</form> </form>
</div> </div>
</div> </div>
<!-- Main Chat Area --> <div style="flex: 1;" class="chat-main-area">
<div style="flex: 1; border: 1px solid #ddd; background: white;" class="chat-main-area"> <div id="chat-content">
<div id="chat-content" style="height: 700px; min-height: 700px; display: flex; flex-direction: column;"> <div id="default-state">
<!-- Default state --> <div class="default-welcome">
<div id="default-state" style="flex: 1; display: flex; align-items: center; justify-content: center; background: #f9f9f9;"> <h3>Welcome to Messenger</h3>
<div style="text-align: center; padding: 60px 20px; color: #666;">
<h3 style="margin: 0 0 10px 0;">Welcome to Messenger</h3>
<p>Select Global Chat or a conversation to start messaging</p> <p>Select Global Chat or a conversation to start messaging</p>
<small style="color: #999;">Click on any channel or conversation in the sidebar</small> <small>Click on any channel or conversation in the sidebar</small>
</div> </div>
</div> </div>
<!-- Chat interface (hidden by default) --> <div id="chat-interface">
<div id="chat-interface" style="display: none; flex: 1; flex-direction: column;"> <div id="chat-header">
<!-- Chat header -->
<div id="chat-header" style="padding: 15px; border-bottom: 1px solid #ddd; background: #f5f5f5; display: flex; justify-content: space-between; align-items: center;">
<div id="chat-title"></div> <div id="chat-title"></div>
<div id="chat-actions"></div> <div id="chat-actions"></div>
</div> </div>
<!-- Messages container --> <div id="messages-container"></div>
<div id="messages-container" style="flex: 1; padding: 20px; overflow-y: auto;">
<!-- Messages will be loaded here -->
</div>
<!-- Message input --> <div id="message-input-area"></div>
<div id="message-input-area" style="padding: 15px; border-top: 1px solid #ddd; background: #f5f5f5;">
<!-- Message input will be loaded here -->
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -101,40 +85,168 @@
</div> </div>
<style> <style>
.messenger-container {
display: flex;
gap: 20px;
margin-top: 20px;
align-items: flex-start;
}
.messaging-sidebar { .messaging-sidebar {
height: fit-content; width: 300px;
flex-shrink: 0;
}
.sidebar-section {
margin-bottom: 20px;
padding: 15px;
background: linear-gradient(135deg, #f8f9fa 0%, #f0f2f5 100%);
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.sidebar-section h3 {
margin: 0 0 15px 0;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #555;
} }
.chat-main-area { .chat-main-area {
height: auto; flex: 1;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border: 1px solid #e0e0e0;
background: white;
}
#chat-content {
height: 600px;
display: flex;
flex-direction: column;
}
#default-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #f5f7fa 0%, #fafbfc 100%);
}
#chat-interface {
display: none;
flex-direction: column;
height: 100%;
}
#chat-header {
padding: 15px;
border-bottom: 1px solid #e0e0e0;
background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
#message-input-area {
padding: 15px;
border-top: 1px solid #e0e0e0;
background: linear-gradient(180deg, #fff 0%, #f8f9fa 100%);
flex-shrink: 0;
} }
#messages-container { #messages-container {
background: #fafafa; flex: 1;
overflow-y: auto;
padding: 20px;
background: linear-gradient(180deg, #f5f7fa 0%, #fafbfc 100%);
scroll-behavior: smooth; scroll-behavior: smooth;
} }
.chat-item {
display: block;
padding: 12px;
margin-bottom: 8px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
text-decoration: none;
color: #333;
cursor: pointer;
transition: all 0.2s ease;
}
.chat-item:hover {
background: #f5f8ff;
border-color: #2196F3;
transform: translateX(3px);
}
.chat-item.active {
background: #e3f2fd;
border-color: #2196F3;
box-shadow: 0 2px 4px rgba(33,150,243,0.2);
}
.chat-item-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-item-title {
display: flex;
align-items: center;
gap: 8px;
}
.chat-item-icon {
font-size: 16px;
width: 24px;
text-align: center;
}
.unread-badge {
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.chat-item-meta {
font-size: 11px;
color: #888;
margin-top: 4px;
}
.message-bubble { .message-bubble {
display: inline-block; display: inline-block;
max-width: 70%; max-width: 70%;
padding: 10px 15px; padding: 12px 16px;
border-radius: 18px; border-radius: 18px;
margin-bottom: 8px; margin-bottom: 4px;
word-wrap: break-word; word-wrap: break-word;
line-height: 1.4; line-height: 1.5;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
} }
.message-bubble.sent { .message-bubble.sent {
background: #2196F3; background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
color: white; color: white;
border-bottom-right-radius: 4px; border-bottom-right-radius: 6px;
} }
.message-bubble.received { .message-bubble.received {
background: white; background: white;
color: #333; color: #333;
border: 1px solid #e0e0e0; border: 1px solid #e8e8e8;
border-bottom-left-radius: 4px; border-bottom-left-radius: 6px;
} }
.message-time { .message-time {
@@ -144,7 +256,7 @@
} }
.message-row { .message-row {
margin-bottom: 15px; margin-bottom: 16px;
display: flex; display: flex;
} }
@@ -157,11 +269,18 @@
} }
.global-message { .global-message {
margin-bottom: 20px; margin-bottom: 16px;
padding: 15px; padding: 16px;
background: white; background: white;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 8px rgba(0,0,0,0.06);
border: 1px solid #eee;
transition: transform 0.2s ease;
}
.global-message:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
} }
.global-message-header { .global-message-header {
@@ -169,18 +288,116 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 10px; margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.global-message-sender {
display: flex;
align-items: center;
gap: 8px;
}
.sender-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 14px;
} }
.global-message-bubble { .global-message-bubble {
color: #333; color: #444;
line-height: 1.5; line-height: 1.6;
word-wrap: break-word; word-wrap: break-word;
} }
.message-input {
flex: 1;
padding: 12px 18px;
border: 2px solid #e0e0e0;
border-radius: 25px;
font-size: 14px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.message-input:focus {
outline: none;
border-color: #2196F3;
box-shadow: 0 0 0 3px rgba(33,150,243,0.1);
}
.send-button {
border-radius: 25px;
padding: 12px 24px;
font-weight: 600;
transition: all 0.2s ease;
}
.send-button:hover {
transform: scale(1.02);
}
.default-welcome {
text-align: center;
padding: 80px 20px;
color: #666;
}
.default-welcome h3 {
margin: 0 0 12px 0;
color: #333;
font-size: 20px;
}
.default-welcome p {
margin: 0 0 8px 0;
color: #666;
}
.default-welcome small {
color: #999;
}
.new-message-form select,
.new-message-form textarea {
width: 100%;
padding: 10px 12px;
margin-bottom: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.2s ease;
}
.new-message-form select:focus,
.new-message-form textarea:focus {
outline: none;
border-color: #2196F3;
}
.new-message-form textarea {
height: 80px;
resize: vertical;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.chat-main-area { .messenger-container {
flex-direction: column;
}
.messaging-sidebar {
width: 100% !important;
}
#chat-content {
height: 500px; height: 500px;
margin-top: 20px;
} }
.message-bubble { .message-bubble {
@@ -193,32 +410,26 @@
let currentChatType = null; let currentChatType = null;
let currentChatId = null; let currentChatId = null;
let messagePollingInterval = null; let messagePollingInterval = null;
let isFirstLoad = true;
// Function to scroll messages container to bottom
function scrollToBottom() { function scrollToBottom() {
const container = document.getElementById('messages-container'); const container = document.getElementById('messages-container');
if (container) { if (container) {
// Small timeout to ensure content is rendered
setTimeout(() => { setTimeout(() => {
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
}, 100); }, 100);
} }
} }
// Handle DOM ready event
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Check URL hash for auto-loading chats
const hash = window.location.hash.substring(1); const hash = window.location.hash.substring(1);
if (hash === 'load-global') { if (hash === 'load-global') {
// Load global chat
setTimeout(() => loadGlobalChat(), 100); setTimeout(() => loadGlobalChat(), 100);
} else if (hash.startsWith('load-conversation-')) { } else if (hash.startsWith('load-conversation-')) {
// Load conversation
const userId = hash.split('-')[2]; const userId = hash.split('-')[2];
if (userId) { if (userId) {
setTimeout(() => { setTimeout(() => {
// Get user name from the conversation list
const conversationEl = document.querySelector('#conversation-' + userId); const conversationEl = document.querySelector('#conversation-' + userId);
if (conversationEl) { if (conversationEl) {
const userName = conversationEl.querySelector('strong').textContent; const userName = conversationEl.querySelector('strong').textContent;
@@ -232,12 +443,11 @@ document.addEventListener('DOMContentLoaded', function() {
function loadGlobalChat() { function loadGlobalChat() {
currentChatType = 'global'; currentChatType = 'global';
currentChatId = 'global'; currentChatId = 'global';
isFirstLoad = true;
// Hide default state, show chat interface
document.getElementById('default-state').style.display = 'none'; document.getElementById('default-state').style.display = 'none';
document.getElementById('chat-interface').style.display = 'flex'; document.getElementById('chat-interface').style.display = 'flex';
// Update header
document.getElementById('chat-title').innerHTML = ` document.getElementById('chat-title').innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;"> <div style="display: flex; align-items: center; gap: 8px;">
<span style="color: #2196F3; font-size: 18px;">#</span> <span style="color: #2196F3; font-size: 18px;">#</span>
@@ -252,33 +462,21 @@ function loadGlobalChat() {
</span> </span>
`; `;
// Highlight active chat
highlightActiveChat('global'); highlightActiveChat('global');
// Load messages
loadGlobalMessages(); loadGlobalMessages();
setTimeout(scrollToBottom, 300);
// Scroll to bottom after loading
setTimeout(() => {
scrollToBottom();
}, 300);
// Setup message input
setupGlobalMessageInput(); setupGlobalMessageInput();
// Start polling for new messages
startMessagePolling(); startMessagePolling();
} }
function loadConversation(userId, userName) { function loadConversation(userId, userName) {
currentChatType = 'conversation'; currentChatType = 'conversation';
currentChatId = userId; currentChatId = userId;
isFirstLoad = true;
// Hide default state, show chat interface
document.getElementById('default-state').style.display = 'none'; document.getElementById('default-state').style.display = 'none';
document.getElementById('chat-interface').style.display = 'flex'; document.getElementById('chat-interface').style.display = 'flex';
// Update header
document.getElementById('chat-title').innerHTML = ` document.getElementById('chat-title').innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;"> <div style="display: flex; align-items: center; gap: 8px;">
<span style="color: #4CAF50; font-size: 18px;">@</span> <span style="color: #4CAF50; font-size: 18px;">@</span>
@@ -293,88 +491,80 @@ function loadConversation(userId, userName) {
</span> </span>
`; `;
// Highlight active chat
highlightActiveChat(userId); highlightActiveChat(userId);
// Load messages
loadConversationMessages(userId); loadConversationMessages(userId);
setTimeout(scrollToBottom, 300);
// Scroll to bottom after loading
setTimeout(() => {
scrollToBottom();
}, 300);
// Setup message input
setupConversationMessageInput(userId); setupConversationMessageInput(userId);
// Start polling for new messages
startMessagePolling(); startMessagePolling();
} }
function highlightActiveChat(chatId) { function highlightActiveChat(chatId) {
// Remove active class from all chats document.querySelectorAll('.chat-item').forEach(el => el.classList.remove('active'));
document.querySelectorAll('[id^="conversation-"], [id="global-chat-link"]').forEach(el => {
el.style.backgroundColor = 'white';
el.style.borderColor = '#ddd';
});
// Add active class to current chat
const activeEl = chatId === 'global' ? const activeEl = chatId === 'global' ?
document.getElementById('global-chat-link') : document.getElementById('global-chat-link') :
document.getElementById('conversation-' + chatId); document.getElementById('conversation-' + chatId);
if (activeEl) { if (activeEl) activeEl.classList.add('active');
activeEl.style.backgroundColor = '#e8f4fd';
activeEl.style.borderColor = '#2196F3';
}
} }
function loadGlobalMessages() { function loadGlobalMessages(silent = false) {
const container = document.getElementById('messages-container'); const container = document.getElementById('messages-container');
if (isFirstLoad && !silent) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #666;">Loading messages...</div>'; container.innerHTML = '<div style="text-align: center; padding: 40px; color: #666;">Loading messages...</div>';
}
fetch('<?= Config::get('URL') ?>message/getGlobalMessages') fetch('<?= Config::get('URL') ?>message/getGlobalMessages')
.then(response => { .then(response => {
console.log('Global messages response status:', response.status); if (!response.ok) throw new Error('HTTP error: ' + response.status);
console.log('Global messages response headers:', response.headers);
if (!response.ok) {
return response.text().then(text => {
console.error('Server returned non-JSON response for global messages:', text);
throw new Error(`HTTP error! status: ${response.status}, response: ${text.substring(0, 200)}`);
});
}
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
if (data.success && data.messages) { if (data.success && data.messages) {
const wasAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 50;
displayGlobalMessages(data.messages); displayGlobalMessages(data.messages);
} else { if (wasAtBottom) scrollToBottom();
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Error loading messages: ' + (data.message || 'Unknown error') + '</div>'; isFirstLoad = false;
} else if (isFirstLoad) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Error loading messages</div>';
} }
}) })
.catch(error => { .catch(error => {
console.error('Error loading global messages:', error); console.error('Error loading global messages:', error);
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Network error: ' + error.message + '</div>'; if (isFirstLoad) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Network error</div>';
}
}); });
} }
function loadConversationMessages(userId) { function loadConversationMessages(userId, silent = false) {
const container = document.getElementById('messages-container'); const container = document.getElementById('messages-container');
if (isFirstLoad && !silent) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #666;">Loading messages...</div>'; container.innerHTML = '<div style="text-align: center; padding: 40px; color: #666;">Loading messages...</div>';
}
fetch(`<?= Config::get('URL') ?>message/getConversationMessages/${userId}`) fetch(`<?= Config::get('URL') ?>message/getConversationMessages/${userId}`)
.then(response => response.json()) .then(response => {
if (!response.ok) throw new Error('HTTP error: ' + response.status);
return response.json();
})
.then(data => { .then(data => {
if (data.success && data.messages) { if (data.success && data.messages) {
const wasAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 50;
displayConversationMessages(data.messages); displayConversationMessages(data.messages);
} else { if (wasAtBottom) scrollToBottom();
isFirstLoad = false;
} else if (isFirstLoad) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Error loading messages</div>'; container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Error loading messages</div>';
} }
}) })
.catch(error => { .catch(error => {
console.error('Error loading conversation messages:', error); console.error('Error loading conversation messages:', error);
if (isFirstLoad) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Network error</div>'; container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Network error</div>';
}
}); });
} }
@@ -383,9 +573,9 @@ function displayGlobalMessages(messages) {
if (messages.length === 0) { if (messages.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div style="text-align: center; padding: 60px 20px;"> <div class="default-welcome">
<h4 style="color: #666;">No messages yet</h4> <h3>No messages yet</h3>
<p style="color: #999;">Start the conversation!</p> <p>Start the conversation!</p>
</div> </div>
`; `;
return; return;
@@ -400,15 +590,17 @@ function displayGlobalMessages(messages) {
}); });
const isOwn = message.sender_id == document.querySelector('meta[name="user-id"]').content; const isOwn = message.sender_id == document.querySelector('meta[name="user-id"]').content;
const initial = message.sender_name.charAt(0).toUpperCase();
return ` return `
<div class="global-message"> <div class="global-message">
<div class="global-message-header"> <div class="global-message-header">
<div style="display: flex; align-items: center; gap: 8px;"> <div class="global-message-sender">
<div class="sender-avatar" style="${isOwn ? 'background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);' : ''}">${initial}</div>
<strong style="color: ${isOwn ? '#2196F3' : '#333'}"> <strong style="color: ${isOwn ? '#2196F3' : '#333'}">
${escapeHtml(message.sender_name)} ${escapeHtml(message.sender_name)}
</strong> </strong>
${isOwn ? '<span style="font-size: 10px; background: #2196F3; color: white; padding: 2px 4px; border-radius: 3px;">You</span>' : ''} ${isOwn ? '<span style="font-size: 10px; background: #2196F3; color: white; padding: 2px 6px; border-radius: 4px;">You</span>' : ''}
</div> </div>
<span class="message-time">${timeString}</span> <span class="message-time">${timeString}</span>
</div> </div>
@@ -419,7 +611,6 @@ function displayGlobalMessages(messages) {
`; `;
}).join(''); }).join('');
// Scroll to bottom
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
} }
@@ -429,9 +620,9 @@ function displayConversationMessages(messages) {
if (messages.length === 0) { if (messages.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div style="text-align: center; padding: 60px 20px;"> <div class="default-welcome">
<h4 style="color: #666;">No messages yet</h4> <h3>No messages yet</h3>
<p style="color: #999;">Send a message to start the conversation!</p> <p>Send a message to start the conversation!</p>
</div> </div>
`; `;
return; return;
@@ -460,7 +651,6 @@ function displayConversationMessages(messages) {
`; `;
}).join(''); }).join('');
// Scroll to bottom
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
} }
@@ -472,9 +662,9 @@ function setupGlobalMessageInput() {
<input type="text" <input type="text"
name="message" name="message"
placeholder="Type your global message..." placeholder="Type your global message..."
style="flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 25px;" class="message-input"
required> required>
<button type="submit" class="button" style="border-radius: 25px; padding: 12px 20px;">Send</button> <button type="submit" class="button send-button">Send</button>
</div> </div>
</form> </form>
`; `;
@@ -488,9 +678,9 @@ function setupConversationMessageInput(userId) {
<input type="text" <input type="text"
name="message" name="message"
placeholder="Type your message..." placeholder="Type your message..."
style="flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 25px;" class="message-input"
required> required>
<button type="submit" class="button" style="border-radius: 25px; padding: 12px 20px;">Send</button> <button type="submit" class="button send-button">Send</button>
</div> </div>
</form> </form>
`; `;
@@ -515,32 +705,20 @@ function sendGlobalMessage(event) {
body: 'message=' + encodeURIComponent(message) body: 'message=' + encodeURIComponent(message)
}) })
.then(response => { .then(response => {
console.log('Global message response status:', response.status); if (!response.ok) throw new Error('HTTP error: ' + response.status);
console.log('Global message response headers:', response.headers);
if (!response.ok) {
return response.text().then(text => {
console.error('Server returned non-JSON response for global message:', text);
throw new Error(`HTTP error! status: ${response.status}, response: ${text.substring(0, 200)}`);
});
}
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
if (data.success) { if (data.success) {
form.reset(); form.reset();
loadGlobalMessages(); // Refresh messages loadGlobalMessages(true);
// Scroll to bottom after sending setTimeout(scrollToBottom, 300);
setTimeout(() => {
scrollToBottom();
}, 500);
} else { } else {
alert('Failed to send message: ' + (data.message || 'Unknown error')); alert('Failed to send message: ' + (data.message || 'Unknown error'));
} }
}) })
.catch(error => { .catch(error => {
console.error('Error sending global message:', error); alert('Network error. Please try again.');
alert('Network error. Please try again. (' + error.message + ')');
}) })
.finally(() => { .finally(() => {
submitButton.textContent = originalText; submitButton.textContent = originalText;
@@ -567,32 +745,20 @@ function sendConversationMessage(event, userId) {
body: 'receiver_id=' + userId + '&message=' + encodeURIComponent(message) body: 'receiver_id=' + userId + '&message=' + encodeURIComponent(message)
}) })
.then(response => { .then(response => {
console.log('Response status:', response.status); if (!response.ok) throw new Error('HTTP error: ' + response.status);
console.log('Response headers:', response.headers);
if (!response.ok) {
return response.text().then(text => {
console.error('Server returned non-JSON response:', text);
throw new Error(`HTTP error! status: ${response.status}, response: ${text.substring(0, 200)}`);
});
}
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
if (data.success) { if (data.success) {
form.reset(); form.reset();
loadConversationMessages(userId); // Refresh messages loadConversationMessages(userId, true);
// Scroll to bottom after sending setTimeout(scrollToBottom, 300);
setTimeout(() => {
scrollToBottom();
}, 500);
} else { } else {
alert('Failed to send message: ' + (data.message || 'Unknown error')); alert('Failed to send message: ' + (data.message || 'Unknown error'));
} }
}) })
.catch(error => { .catch(error => {
console.error('Error sending conversation message:', error); alert('Network error. Please try again.');
alert('Network error. Please try again. (' + error.message + ')');
}) })
.finally(() => { .finally(() => {
submitButton.textContent = originalText; submitButton.textContent = originalText;
@@ -601,19 +767,15 @@ function sendConversationMessage(event, userId) {
} }
function startMessagePolling() { function startMessagePolling() {
// Clear existing polling if (messagePollingInterval) clearInterval(messagePollingInterval);
if (messagePollingInterval) {
clearInterval(messagePollingInterval);
}
// Start new polling
messagePollingInterval = setInterval(() => { messagePollingInterval = setInterval(() => {
if (currentChatType === 'global') { if (currentChatType === 'global') {
loadGlobalMessages(); loadGlobalMessages(true);
} else if (currentChatType === 'conversation') { } else if (currentChatType === 'conversation') {
loadConversationMessages(currentChatId); loadConversationMessages(currentChatId, true);
} }
}, 3000); // Poll every 3 seconds }, 3000);
} }
function escapeHtml(text) { function escapeHtml(text) {
@@ -622,7 +784,6 @@ function escapeHtml(text) {
return div.innerHTML; return div.innerHTML;
} }
// Handle new message form submission
document.getElementById('message-form').addEventListener('submit', function(e) { document.getElementById('message-form').addEventListener('submit', function(e) {
e.preventDefault(); e.preventDefault();
@@ -642,13 +803,11 @@ document.getElementById('message-form').addEventListener('submit', function(e) {
if (data.success) { if (data.success) {
this.reset(); this.reset();
showFeedback('Message sent successfully!', 'success'); showFeedback('Message sent successfully!', 'success');
// Could refresh conversation list here
} else { } else {
showFeedback(data.message || 'Failed to send message', 'error'); showFeedback(data.message || 'Failed to send message', 'error');
} }
}) })
.catch(error => { .catch(error => {
console.error('Error sending message:', error);
showFeedback('Network error. Please try again.', 'error'); showFeedback('Network error. Please try again.', 'error');
}) })
.finally(() => { .finally(() => {
@@ -659,7 +818,6 @@ document.getElementById('message-form').addEventListener('submit', function(e) {
function showFeedback(message, type) { function showFeedback(message, type) {
const feedback = document.createElement('div'); const feedback = document.createElement('div');
feedback.className = `feedback-${type}`;
feedback.textContent = message; feedback.textContent = message;
feedback.style.cssText = ` feedback.style.cssText = `
position: fixed; position: fixed;
@@ -682,14 +840,12 @@ function showFeedback(message, type) {
}, 3000); }, 3000);
} }
// Add CSS for animations
const animationStyle = document.createElement('style'); const animationStyle = document.createElement('style');
animationStyle.textContent = ` animationStyle.textContent = `
@keyframes slideIn { @keyframes slideIn {
from { transform: translateX(100%); opacity: 0; } from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; } to { transform: translateX(0); opacity: 1; }
} }
@keyframes slideOut { @keyframes slideOut {
from { transform: translateX(0); opacity: 1; } from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; } to { transform: translateX(100%); opacity: 0; }