This commit is contained in:
2026-01-10 17:22:49 +01:00
parent edcc1b5403
commit 674fabb715
21 changed files with 2135 additions and 489 deletions

285
README.md
View File

@@ -1,220 +1,155 @@
# HUGE Installation und Setup # HUGE - Elias Fähnrich
## Schnellstart ---
- Voraussetzungen prüfen (siehe unten) ## Anpassung der Useranmeldung
- Abhängigkeiten installieren
- Datenbank anlegen
- Webserver auf `public/` zeigen lassen
- Starten und testen
Befehle (Beispiele): #### Registrierung ohne Captcha und E-Mail-Verifikation
```bash **Beschreibung:**
# Composer installieren (macOS über Homebrew) Die Registrierung wurde so angepasst, dass Benutzer ohne Captcha und E-Mail-Verifikation registriert werden können. Benutzer werden nach der Registrierung automatisch aktiviert.
brew install composer
# Abhängigkeiten holen (im Projektordner) **Technische Umsetzung:**
composer install - Entfernung des Captcha-Features aus dem Registrierungsprozess
- Deaktivierung der E-Mail-Verifizierung
- Automatische Aktivierung neuer Benutzerkonten
- Vereinfachung des Registrierungsformulars
# Datenbank & Tabellen anlegen **Admin-Funktion:**
mysql -u root -p < application/_installation/01-create-database.sql - Nur Administratoren können über das gleiche Formular neue Benutzer anlegen
mysql -u root -p < application/_installation/02-create-table-users.sql - Sicherstellung, dass nur autorisierte Personen Benutzerkonten erstellen können
mysql -u root -p < application/_installation/03-create-table-notes.sql
# Rechte für Avatare (je nach OS anpassen) ```
# Ubuntu/Debian: <!-- Screenshot Platzhalter -->
sudo chown -R www-data:www-data public/avatars [📸 Screenshot: Vereinfachtes Registrierungsformular]
sudo chmod -R 775 public/avatars [📸 Screenshot: Admin-Benutzererstellung]
[📸 Screenshot: Automatisch aktivierter Benutzer]
# macOS (Apache Standardnutzer _www):
sudo chown -R _www:_www public/avatars
sudo chmod -R 775 public/avatars
``` ```
--- ---
## Voraussetzungen ## Erweiterung der Admin-Funktionen
- PHP >= 5.5 (laut composer.json) #### 👥 Benutzergruppen-Verwaltung
- Composer
- Apache 2.4 mit `mod_rewrite`
- MySQL/MariaDB
- PHP-Erweiterungen: PDO + pdo_mysql, OpenSSL, mbstring
- Schreibrechte für `public/avatars/`
--- **Beschreibung:**
Implementierung einer erweiterten Benutzerverwaltung mit Gruppen-System. Anstelle des einfachen Typen-Feldes wurde eine vollwertige Gruppenverwaltung eingeführt.
## Projekt beziehen **Gruppenstruktur:**
- **Admin (Typ 7)**: Voll administrative Rechte
- **Gast (Typ 1)**: Leserechte für öffentliche Inhalte
- **Normaler Benutzer (Typ 2)**: Standard-Benutzerrechte
- **Typen 3-6**: Reserviert für zukünftige Gruppenerweiterungen
- Repository klonen oder entpacken **Technische Umsetzung:**
- In den Projektordner wechseln - Erstellung einer neuen Tabelle `user_groups` zur Gruppendefinition
- Zuweisung von Gruppennamen zu den Typen
- Integration in die Benutzerverwaltung des Admins
--- #### Öffentliches Benutzerverzeichnis mit DataTables
## Abhängigkeiten installieren (Composer) **Beschreibung:**
Implementierung einer öffentlichen Benutzerliste, die alle Benutzer und ihre Gruppen anzeigt. Diese Liste ist für alle zugänglich, jedoch schreibgeschützt.
- Im Projektordner ausführen: **Technische Features:**
- Integration von DataTables/jQuery für interaktive Tabellen
- Sortier- und Filterfunktionen
- Paginierung der Benutzerliste
- Responsive Darstellung auf verschiedenen Geräten
```bash **Zugriffsrechte:**
composer install - **Öffentlicher Zugriff**: Nur Anzeige der Benutzerliste
- **Admin-Zugriff**: Vollständige Verwaltungsfunktionen
```
<!-- Screenshot Platzhalter -->
[📸 Screenshot: Admin-Gruppenzuweisung]
[📸 Screenshot: Öffentliches Benutzerverzeichnis mit DataTables]
[📸 Screenshot: jQuery DataTables in Aktion]
``` ```
--- ---
## Webserver konfigurieren (Apache) ## jQuery - Einführung und Grundlagen
- DocumentRoot auf `.../huge/public` setzen #### JavaScript Basics
- `AllowOverride All` aktivieren (damit `.htaccess` greift) jQuery ist eine JavaScript-Bibliothek, die grundlegende JavaScript-Kenntnisse voraussetzt. Wichtige Grundlagen umfassen:
- `mod_rewrite` aktivieren
- Apache neu starten
Minimalbeispiel VirtualHost: - DOM-Manipulation
- Event-Handling
- Asynchrone Programmierung
- Objektorientierte Programmierung
```apacheconf #### jQuery Grundlagen
<VirtualHost *:80> Die jQuery-Bibliothek ist zentral für die Interaktivität der Benutzeroberfläche:
ServerName huge.local
DocumentRoot "/pfad/zu/huge/public"
<Directory "/pfad/zu/huge/public"> **Einbindung:**
AllowOverride All ```html
Require all granted <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</Directory>
SetEnv APPLICATION_ENV development
ErrorLog "${APACHE_LOG_DIR}/huge_error.log"
CustomLog "${APACHE_LOG_DIR}/huge_access.log" combined
</VirtualHost>
``` ```
Hinweis: `.htaccess` leitet auf `public/index.php` um. **Zentrale Elemente:**
- **Dollarzeichen ($)**: Hauptselektor für DOM-Elemente
- **Basis-Syntax**: `$("element")`
- **Document Ready**: `$(document).ready(function(){ ... })`
--- **Wichtige Selektoren:**
```javascript
// ID-Selektor (schnellste Methode)
$("#elementId")
## Datenbank anlegen // Klassen-Selektor
$(".klassenname")
- SQL-Skripte in dieser Reihenfolge ausführen (`application/_installation/`): // Multi-Selektor
- `01-create-database.sql` $("div, p, a, span")
- `02-create-table-users.sql`
- `03-create-table-notes.sql`
Beispiel in der Shell: // Komplexe Selektoren
$("#container .item:first-child")
```bash
mysql -u root -p < application/_installation/01-create-database.sql
mysql -u root -p < application/_installation/02-create-table-users.sql
mysql -u root -p < application/_installation/03-create-table-notes.sql
``` ```
--- **DOM-Manipulation:**
```javascript
// Einzelne Eigenschaft ändern
$("#test").css("color", "#FFFFFF");
## Framework-Konfiguration (Entwicklung) // Mehrere Eigenschaften ändern
$("#test").css({
"color": "#FFFFFF",
"height": "25px"
});
- Datei: `application/config/config.development.php` // Methodenverkettung (Chaining)
- Wichtige Schlüssel: $("#test")
- URL .css({ "color": "#FFFFFF", "height": "25px" })
- `URL` (Basis-URL, endet mit `/`; auto-detect möglich) .html("Neuer Text")
- Pfade .show();
- `PATH_CONTROLLER`, `PATH_VIEW` (meist unverändert)
- Routing
- `DEFAULT_CONTROLLER`, `DEFAULT_ACTION`
- Datenbank
- `DB_TYPE`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS`, `DB_CHARSET`
- Captcha
- `CAPTCHA_WIDTH`, `CAPTCHA_HEIGHT`
- Cookies & Session
- `COOKIE_RUNTIME`, `COOKIE_PATH`, `COOKIE_DOMAIN`, `COOKIE_SECURE`, `COOKIE_HTTP`
- `SESSION_RUNTIME`
- Avatare/Gravatar
- `USE_GRAVATAR`, `GRAVATAR_DEFAULT_IMAGESET`, `GRAVATAR_RATING`
- `AVATAR_SIZE`, `AVATAR_JPEG_QUALITY`, `AVATAR_DEFAULT_IMAGE`
- Ordner `public/avatars/` beschreibbar machen
- Sicherheit
- `ENCRYPTION_KEY`, `HMAC_SALT` (für eigene Installation ändern)
- E-Mail
- `EMAIL_USED_MAILER`, `EMAIL_USE_SMTP`
- `EMAIL_SMTP_HOST`, `EMAIL_SMTP_AUTH`, `EMAIL_SMTP_USERNAME`, `EMAIL_SMTP_PASSWORD`, `EMAIL_SMTP_PORT`, `EMAIL_SMTP_ENCRYPTION`
- `EMAIL_PASSWORD_RESET_*`, `EMAIL_VERIFICATION_*`
---
## Umgebungen (Environment)
- Klasse: `application/core/Environment.php`
- Ermittelt `APPLICATION_ENV` (Fallback: `development`)
- Weitere Datei möglich: `config.production.php`
- Apache-Variable setzen:
```apacheconf
SetEnv APPLICATION_ENV production
``` ```
--- #### DataTables Integration
DataTables erweitert HTML-Tabellen um leistungsstarke Funktionen:
## Verzeichnisrechte ```javascript
$(document).ready(function() {
- `public/avatars/` beschreibbar $('#benutzerTabelle').DataTable({
- Optional Logs/Uploads je nach Bedarf "language": {
"url": "//cdn.datatables.net/plug-ins/1.10.25/i18n/German.json"
--- },
"pageLength": 25,
## Starten "responsive": true
});
- Browser: `http://<host>/` });
- Registrierung/Login testen
- Mailversand nur mit korrekt konfiguriertem SMTP
---
## Tests ausführen
- PHPUnit unter `vendor/bin/phpunit`
- Konfiguration: `tests/phpunit.xml`
```bash
vendor/bin/phpunit -c tests/phpunit.xml
``` ```
--- **Features:**
- Automatische Sortierung aller Spalten
## Häufige Probleme - Suchfunktion in Echtzeit
- Paginierung mit anpassbarer Seitenlänge
- 404 bei allen Routen - Responsive Darstellung auf mobilen Geräten
- `mod_rewrite` aktivieren - Mehrsprachige Unterstützung (Deutsch implementiert)
- `AllowOverride All` setzen
- DocumentRoot auf `public/`
- Falsche Links/Assets
- `URL` in der Config prüfen
- Datenbankfehler
- `DB_*`-Werte und Rechte prüfen
- E-Mails kommen nicht an
- `EMAIL_USE_SMTP=true` und SMTP-Daten prüfen
- Avatare fehlen
- Schreibrechte für `public/avatars/`
--- ---
## Option: Vagrant „One-Click-Installation“ <div align="center">
<img src="./assets/footer/gray0_ctp_on_line.svg" alt="Gadze"/>
- Ordner: `_one-click-installation/` </div>
- Voraussetzungen: Vagrant + VirtualBox
```bash
cd _one-click-installation
vagrant up
```
- Projekt über die VM nutzen (Host/Ports laut Vagrant-Ausgabe)
---
## Struktur (Kurz)
- `public/` (Webroot, `.htaccess`, `index.php`)
- `application/controller/` (Controller)
- `application/model/` (Modelle)
- `application/view/` (Views)
- `application/config/` (Konfiguration je Environment)
- `application/_installation/` (SQL-Skripte)
- `vendor/` (Composer-Abhängigkeiten)
- `tests/` (PHPUnit)

View File

@@ -2,5 +2,10 @@ CREATE TABLE IF NOT EXISTS `huge`.`notes` (
`note_id` int(11) unsigned NOT NULL AUTO_INCREMENT, `note_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`note_text` text NOT NULL, `note_text` text NOT NULL,
`user_id` int(11) unsigned NOT NULL, `user_id` int(11) unsigned NOT NULL,
`note_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`note_id`) PRIMARY KEY (`note_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user notes'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user notes';
-- Foreign key constraint
ALTER TABLE `notes`
ADD CONSTRAINT `notes_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE;

View File

@@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS `messages` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`sender_id` int(11) NOT NULL, `sender_id` int(11) NOT NULL,
`receiver_id` int(11) DEFAULT NULL, `receiver_id` int(11) DEFAULT NULL,
`group_type` enum('admins','moderators','all_users') DEFAULT NULL, `group_type` enum('admins','moderators','all_users','global') DEFAULT NULL,
`subject` varchar(255) NOT NULL, `subject` varchar(255) NOT NULL,
`message` text NOT NULL, `message` text NOT NULL,
`is_read` tinyint(1) NOT NULL DEFAULT 0, `is_read` tinyint(1) NOT NULL DEFAULT 0,
@@ -11,7 +11,8 @@ CREATE TABLE IF NOT EXISTS `messages` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `sender_id` (`sender_id`), KEY `sender_id` (`sender_id`),
KEY `receiver_id` (`receiver_id`), KEY `receiver_id` (`receiver_id`),
KEY `is_read` (`is_read`) KEY `is_read` (`is_read`),
KEY `group_type` (`group_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
-- Foreign key constraints -- Foreign key constraints

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' => '127.0.0.1', 'DB_HOST' => 'localhost',
'DB_NAME' => 'huge', 'DB_NAME' => 'huge',
'DB_USER' => 'root', 'DB_USER' => 'root',
'DB_PASS' => 'root', 'DB_PASS' => 'root',

View File

@@ -10,6 +10,15 @@ class MessageController extends Controller
Auth::checkAuthentication(); Auth::checkAuthentication();
} }
/**
* 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';
}
/** /**
* Send a message to a specific user via URL parameters * Send a message to a specific user via URL parameters
* URL format: message/send/{receiver_id}/{subject}/{message} * URL format: message/send/{receiver_id}/{subject}/{message}
@@ -23,6 +32,13 @@ class MessageController extends Controller
$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()) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'Receiver and message are required']);
return;
}
Session::add('feedback_negative', 'Receiver and message are required'); Session::add('feedback_negative', 'Receiver and message are required');
Redirect::to('message'); Redirect::to('message');
return; return;
@@ -32,6 +48,18 @@ class MessageController extends Controller
$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()) {
header('Content-Type: application/json');
if ($success) {
echo json_encode(['success' => true, 'message' => 'Message sent successfully']);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to send message']);
}
return;
}
// Regular request handling
if ($success) { if ($success) {
Session::add('feedback_positive', 'Message sent successfully'); Session::add('feedback_positive', 'Message sent successfully');
} else { } else {
@@ -150,6 +178,64 @@ class MessageController extends Controller
} }
} }
/**
* Handle reply to a message
*/
public function reply()
{
// Always return JSON for this endpoint
header('Content-Type: application/json');
// Start output buffering to catch any accidental output
ob_start();
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Invalid request method']);
exit();
}
$receiver_id = isset($_POST['receiver_id']) ? $_POST['receiver_id'] : null;
$message = isset($_POST['message']) ? $_POST['message'] : null;
if (!$receiver_id || !$message) {
echo json_encode(['success' => false, 'message' => 'Receiver and message are required']);
exit();
}
$sender_id = Session::get('user_id');
if (!$sender_id) {
echo json_encode(['success' => false, 'message' => 'Not logged in']);
exit();
}
// Send the message (using sendToUser without subject)
$success = MessageModel::sendToUser($sender_id, $receiver_id, 'Re: Message', $message);
if ($success) {
echo json_encode(['success' => true, 'message' => 'Reply sent successfully']);
} else {
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();
}
/**
* Show global chat interface
*/
public function global()
{
// Redirect to main messages page with global chat hash
Redirect::to('message#load-global');
}
/** /**
* Show the messenger interface * Show the messenger interface
*/ */
@@ -189,13 +275,95 @@ class MessageController extends Controller
return; return;
} }
// Redirect to main messages page with conversation hash
Redirect::to('message#load-conversation-' . $other_user_id);
}
/**
* Get conversation messages as JSON (AJAX endpoint)
*/
public function getConversationMessages()
{
$user_id = Session::get('user_id');
$url_parts = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
$other_user_id = isset($url_parts[2]) ? $url_parts[2] : null;
if (!$other_user_id) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'Missing user ID']);
return;
}
// Get messages // Get messages
$messages = MessageModel::getMessagesWithUser($user_id, $other_user_id); $messages = MessageModel::getMessagesWithUser($user_id, $other_user_id);
$this->View->render('message/conversation', array( // Mark messages as read when loading the conversation
'messages' => $messages, MessageModel::markAsRead($user_id, $other_user_id);
'other_user' => $other_user
)); 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();
}
/**
* Send message to global chat
*/
public function sendToGlobal()
{
// Always return JSON for this endpoint
header('Content-Type: application/json');
// Start output buffering to catch any accidental output
ob_start();
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Invalid request method']);
exit();
}
$message = isset($_POST['message']) ? $_POST['message'] : null;
$sender_id = Session::get('user_id');
if (!$message) {
echo json_encode(['success' => false, 'message' => 'Message is required']);
exit();
}
if (!$sender_id) {
echo json_encode(['success' => false, 'message' => 'Not logged in']);
exit();
}
$success = MessageModel::sendToGlobal($sender_id, $message);
if ($success) {
echo json_encode(['success' => true, 'message' => 'Message sent to global chat']);
} else {
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();
} }
/** /**

View File

@@ -3,6 +3,8 @@
/** /**
* The note controller: Just an example of simple create, read, update and delete (CRUD) actions. * The note controller: Just an example of simple create, read, update and delete (CRUD) actions.
*/ */
require_once __DIR__ . '/../libs/SimpleMarkdown.php';
class NoteController extends Controller class NoteController extends Controller
{ {
/** /**
@@ -29,6 +31,23 @@ class NoteController extends Controller
)); ));
} }
/**
* Get note as JSON (AJAX endpoint)
*/
public function getNote($note_id)
{
$note = NoteModel::getNote($note_id);
header('Content-Type: application/json');
if ($note) {
// Add markdown version
$note->note_html = SimpleMarkdown::parse($note->note_text);
echo json_encode(['success' => true, 'note' => $note]);
} else {
echo json_encode(['success' => false, 'message' => 'Note not found']);
}
}
/** /**
* This method controls what happens when you move to /dashboard/create in your app. * This method controls what happens when you move to /dashboard/create in your app.
* Creates a new note. This is usually the target of form submit actions. * Creates a new note. This is usually the target of form submit actions.
@@ -36,7 +55,18 @@ class NoteController extends Controller
*/ */
public function create() public function create()
{ {
NoteModel::createNote(Request::post('note_text')); $success = NoteModel::createNote(Request::post('note_text'));
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if ($success) {
echo json_encode(['success' => true, 'message' => 'Note created successfully']);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to create note']);
}
return;
}
Redirect::to('note'); Redirect::to('note');
} }
@@ -59,7 +89,18 @@ class NoteController extends Controller
*/ */
public function editSave() public function editSave()
{ {
NoteModel::updateNote(Request::post('note_id'), Request::post('note_text')); $success = NoteModel::updateNote(Request::post('note_id'), Request::post('note_text'));
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if ($success) {
echo json_encode(['success' => true, 'message' => 'Note updated successfully']);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to update note']);
}
return;
}
Redirect::to('note'); Redirect::to('note');
} }
@@ -71,7 +112,27 @@ class NoteController extends Controller
*/ */
public function delete($note_id) public function delete($note_id)
{ {
NoteModel::deleteNote($note_id); $success = NoteModel::deleteNote($note_id);
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
if ($success) {
echo json_encode(['success' => true, 'message' => 'Note deleted successfully']);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to delete note']);
}
return;
}
Redirect::to('note'); Redirect::to('note');
} }
/**
* 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

@@ -17,6 +17,15 @@ class View
public $avatar_file_path; public $avatar_file_path;
public $user_account_type; public $user_account_type;
/* Messenger properties */
public $conversations;
public $unread_count;
public $all_users;
/* Note properties */
public $messages;
public $other_user;
/** /**
* Static property to track if header has been rendered * Static property to track if header has been rendered
*/ */

View File

@@ -0,0 +1,102 @@
<?php
/**
* Simple Markdown Parser
* Supports basic markdown syntax:
* # Headers
* **bold** *italic* `code`
* - lists
* [links](url)
* > quotes
*/
class SimpleMarkdown
{
/**
* Parse markdown text to HTML
*/
public static function parse($text)
{
// Convert special characters to HTML entities first
$text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
// Headers (h1-h6)
$text = preg_replace('/^#{1,6}\s+(.+)$/m', '<h3>$1</h3>', $text);
// Bold text
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
// Italic text
$text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
// Inline code
$text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text);
// Code blocks
$text = preg_replace('/```(.+?)```/s', '<pre><code>$1</code></pre>', $text);
// Links
$text = preg_replace('/\[(.+?)\]\((.+?)\)/', '<a href="$2" target="_blank">$1</a>', $text);
// Blockquotes
$text = preg_replace('/^>\s+(.+)$/m', '<blockquote>$1</blockquote>', $text);
// Unordered lists
$text = preg_replace('/^\-\s+(.+)$/m', '<li>$1</li>', $text);
$text = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $text);
// Line breaks
$text = nl2br($text);
// Clean up multiple consecutive line breaks
$text = preg_replace('/(<br\s*\/?>\s*){3,}/', '<br><br>', $text);
return $text;
}
/**
* Convert HTML back to markdown (simplified)
* This is a basic implementation - may not handle all cases perfectly
*/
public static function toMarkdown($html)
{
// Basic HTML to markdown conversion
$markdown = $html;
// Headers
$markdown = preg_replace('/<h[1-6]>(.+?)<\/h[1-6]>/i', '# $1', $markdown);
// Bold
$markdown = preg_replace('/<strong>(.+?)<\/strong>/i', '**$1**', $markdown);
// Italic
$markdown = preg_replace('/<em>(.+?)<\/em>/i', '*$1*', $markdown);
// Code
$markdown = preg_replace('/<code>(.+?)<\/code>/i', '`$1`', $markdown);
$markdown = preg_replace('/<pre><code>(.+?)<\/code><\/pre>/is', "```$1```", $markdown);
// Links
$markdown = preg_replace('/<a href="(.+?)"(?:\s+target="_blank")?>(.+?)<\/a>/i', '[$2]($1)', $markdown);
// Blockquotes
$markdown = preg_replace('/<blockquote>(.+?)<\/blockquote>/i', '> $1', $markdown);
// Lists (simplified)
$markdown = preg_replace('/<ul>(.+?)<\/ul>/is', '$1', $markdown);
$markdown = preg_replace('/<li>(.+?)<\/li>/i', '- $1', $markdown);
// Line breaks
$markdown = preg_replace('/<br\s*\/?>/i', "\n", $markdown);
// Decode HTML entities
$markdown = htmlspecialchars_decode($markdown, ENT_QUOTES);
// Clean up
$markdown = preg_replace('/\n{3,}/', "\n\n", $markdown);
$markdown = trim($markdown);
return $markdown;
}
}
?>

View File

@@ -6,8 +6,8 @@ class MessageModel
{ {
$database = DatabaseFactory::getFactory()->getConnection(); $database = DatabaseFactory::getFactory()->getConnection();
$sql = "INSERT INTO messages (sender_id, receiver_id, group_type, subject, message) $sql = "INSERT INTO messages (sender_id, receiver_id, group_type, subject, message, is_read)
VALUES (:sender_id, :receiver_id, :group_type, :subject, :message)"; VALUES (:sender_id, :receiver_id, :group_type, :subject, :message, 0)";
$query = $database->prepare($sql); $query = $database->prepare($sql);
return $query->execute(array( return $query->execute(array(
':sender_id' => $sender_id, ':sender_id' => $sender_id,
@@ -170,6 +170,36 @@ class MessageModel
return $query->fetchAll(); return $query->fetchAll();
} }
public static function getGlobalMessages()
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT m.*, u.user_name as sender_name,
'received' as message_type
FROM messages m
JOIN users u ON m.sender_id = u.user_id
WHERE m.group_type = 'global'
AND m.receiver_id IS NULL
ORDER BY m.created_at ASC";
$query = $database->prepare($sql);
$query->execute();
return $query->fetchAll();
}
public static function sendToGlobal($sender_id, $message)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "INSERT INTO messages (sender_id, group_type, subject, message, is_read)
VALUES (:sender_id, 'global', 'Global Chat', :message, 1)";
$query = $database->prepare($sql);
return $query->execute(array(
':sender_id' => $sender_id,
':message' => $message
));
}
public static function getAllUsers($current_user_id) public static function getAllUsers($current_user_id)
{ {
$database = DatabaseFactory::getFactory()->getConnection(); $database = DatabaseFactory::getFactory()->getConnection();

View File

@@ -14,12 +14,20 @@ class NoteModel
{ {
$database = DatabaseFactory::getFactory()->getConnection(); $database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, note_id, note_text FROM notes WHERE user_id = :user_id"; $sql = "SELECT note_id, note_text, note_timestamp FROM notes WHERE user_id = :user_id ORDER BY note_timestamp DESC";
$query = $database->prepare($sql); $query = $database->prepare($sql);
$query->execute(array(':user_id' => Session::get('user_id'))); $query->execute(array(':user_id' => Session::get('user_id')));
// fetchAll() is the PDO method that gets all result rows // fetchAll() is the PDO method that gets all result rows
return $query->fetchAll(); $notes = $query->fetchAll();
// Add markdown HTML to each note
require_once __DIR__ . '/../libs/SimpleMarkdown.php';
foreach ($notes as &$note) {
$note->note_html = SimpleMarkdown::parse($note->note_text);
}
return $notes;
} }
/** /**
@@ -29,13 +37,21 @@ class NoteModel
*/ */
public static function getNote($note_id) public static function getNote($note_id)
{ {
$database = DatabaseFactory::getFactory()->getConnectionWithMySQLI(); $database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, note_id, note_text FROM notes WHERE user_id = :user_id AND note_id = :note_id LIMIT 1"; $sql = "SELECT note_id, note_text, note_timestamp FROM notes WHERE user_id = :user_id AND note_id = :note_id LIMIT 1";
$query = $database->prepare($sql); $query = $database->prepare($sql);
$query->execute(array(':user_id' => Session::get('user_id'), ':note_id' => $note_id)); $query->execute(array(':user_id' => Session::get('user_id'), ':note_id' => $note_id));
return $query; $result = $query->fetch();
// Add markdown HTML
if ($result) {
require_once __DIR__ . '/../libs/SimpleMarkdown.php';
$result->note_html = SimpleMarkdown::parse($result->note_text);
}
return $result ? $result : null;
} }
/** /**

View File

@@ -1,115 +1,17 @@
<div class="container"> <?php $this->render('_templates/header'); ?>
<div class="row">
<!-- Back to Messenger -->
<div class="col-md-12">
<a href="<?= Config::get('URL') ?>message" class="btn btn-default">
<span class="glyphicon glyphicon-arrow-left"></span> Back to Messenger
</a>
<hr>
</div>
</div>
<div class="row"> <div class="box">
<!-- Chat Area --> <?php $this->renderFeedbackMessages(); ?>
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
Conversation with <?= htmlspecialchars($this->other_user->user_name) ?>
</div>
<div class="panel-body message-container">
<?php if (empty($this->messages)): ?>
<div class="text-center">
<em>No messages yet. Start a conversation!</em>
</div>
<?php else: ?>
<?php foreach ($this->messages as $msg): ?>
<div class="message <?= $msg->message_type ?>">
<div class="message-bubble">
<?= htmlspecialchars($msg->message) ?>
</div>
<div class="message-time">
<?= date('M j, Y H:i', strtotime($msg->created_at)) ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- Reply Form --> <script>
<div class="panel-footer"> // Redirect to main messages page and load conversation
<form action="<?= Config::get('URL') ?>message/send" method="post" id="reply-form"> window.location.href = '<?= Config::get('URL') ?>message' + '#load-conversation-<?= isset($this->other_user) ? $this->other_user->user_id : '' ?>';
<input type="hidden" name="receiver_id" value="<?= $this->other_user->user_id ?>"> </script>
<div class="input-group">
<input type="text" name="message" class="form-control" placeholder="Type your message..." required> <div style="text-align: center; padding: 60px;">
<span class="input-group-btn"> <h3>Redirecting to Conversation...</h3>
<button type="submit" class="btn btn-primary">Send</button> <p>If you're not redirected automatically, <a href="<?= Config::get('URL') ?>message">click here</a>.</p>
</span>
</div>
</form>
</div>
</div>
</div>
</div> </div>
</div> </div>
<style>
.message-container {
height: 400px;
overflow-y: auto;
background-color: #f5f5f5;
padding: 15px;
}
.message {
margin-bottom: 15px;
clear: both;
}
.message.sent {
text-align: right;
}
.message.received {
text-align: left;
}
.message-bubble {
display: inline-block;
max-width: 70%;
padding: 10px 15px;
border-radius: 18px;
position: relative;
word-wrap: break-word;
}
.message.sent .message-bubble {
background-color: #007bff;
color: white;
}
.message.received .message-bubble {
background-color: #e5e5ea;
color: black;
}
.message-time {
font-size: 0.8em;
color: #666;
margin-top: 5px;
}
/* Scroll to bottom of messages on load */
.message-container {
scroll-behavior: smooth;
}
</style>
<script>
// Scroll to bottom of messages
document.addEventListener('DOMContentLoaded', function() {
const container = document.querySelector('.message-container');
container.scrollTop = container.scrollHeight;
});
</script>
<?php $this->render('_templates/footer'); ?> <?php $this->render('_templates/footer'); ?>

View File

@@ -0,0 +1,17 @@
<?php $this->render('_templates/header'); ?>
<div class="box">
<?php $this->renderFeedbackMessages(); ?>
<script>
// Redirect to main messages page and load global chat
window.location.href = '<?= Config::get('URL') ?>message' + '#load-global';
</script>
<div style="text-align: center; padding: 60px;">
<h3>Redirecting to Global Chat...</h3>
<p>If you're not redirected automatically, <a href="<?= Config::get('URL') ?>message">click here</a>.</p>
</div>
</div>
<?php $this->render('_templates/footer'); ?>

View File

@@ -1,106 +1,99 @@
<div class="container"> <div class="box">
<h1>Messenger</h1> <h1>Messenger</h1>
<?php $this->renderFeedbackMessages(); ?>
<div class="row"> <!-- Add user ID for JavaScript -->
<!-- Conversations List --> <meta name="user-id" content="<?= Session::get('user_id') ?>">
<div class="col-md-4">
<div class="panel panel-default"> <script src="<?= Config::get('URL') ?>public/js/messaging.js"></script>
<div class="panel-heading">
Conversations <div style="display: flex; gap: 20px; margin-top: 20px;">
<?php if ($this->unread_count > 0): ?> <!-- Left Sidebar -->
<span class="badge pull-right"><?= $this->unread_count ?></span> <div style="width: 300px;" class="messaging-sidebar">
<?php endif; ?> <div style="margin-bottom: 20px; padding: 15px; background: #f5f5f5; border: 1px solid #ddd;">
<h3 style="margin: 0 0 15px 0;">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 style="display: flex; align-items: center; gap: 8px;">
<span style="color: #2196F3;">#</span>
<strong>Global Chat</strong>
</div> </div>
<div class="panel-body" style="padding: 0; max-height: 500px; overflow-y: auto;"> <small style="color: #666; font-size: 11px;">Public chatroom</small>
<div class="list-group"> </div>
</div>
<div style="margin-bottom: 20px; padding: 15px; background: #f5f5f5; border: 1px solid #ddd;">
<h3 style="margin: 0 0 15px 0;">Direct Messages</h3>
<?php if (empty($this->conversations)): ?> <?php if (empty($this->conversations)): ?>
<div class="list-group-item"> <p style="text-align: center; padding: 20px; background: white; border: 1px solid #ddd;">No conversations yet</p>
<em>No conversations yet</em>
</div>
<?php else: ?> <?php else: ?>
<?php foreach ($this->conversations as $conv): ?> <?php foreach ($this->conversations as $conv): ?>
<a href="<?= Config::get('URL') ?>message/conversation/<?= $conv->other_user_id ?>" <div onclick="loadConversation(<?= $conv->other_user_id ?>, '<?= htmlspecialchars(addslashes($conv->user_name)) ?>')"
class="list-group-item <?= $conv->unread_count > 0 ? 'active' : '' ?>"> 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;"
<h5 class="list-group-item-heading"> onmouseover="this.style.backgroundColor='#f0f0f0'"
<?= htmlspecialchars($conv->user_name) ?> onmouseout="this.style.backgroundColor='white'"
id="conversation-<?= $conv->other_user_id ?>">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="color: #4CAF50;">@</span>
<strong><?= htmlspecialchars($conv->user_name) ?></strong>
</div>
<?php if ($conv->unread_count > 0): ?> <?php if ($conv->unread_count > 0): ?>
<span class="badge"><?= $conv->unread_count ?></span> <span style="background: #f44336; color: white; padding: 2px 6px; border-radius: 10px; font-size: 11px;">
<?= $conv->unread_count ?>
</span>
<?php endif; ?> <?php endif; ?>
</h5> </div>
<p class="list-group-item-text"> <small style="color: #666; font-size: 11px;"><?= date('M j, H:i', strtotime($conv->last_message_time)) ?></small>
<small><?= date('M j, Y', strtotime($conv->last_message_time)) ?></small> </div>
</p>
</a>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div>
</div>
<!-- New Message --> <div style="padding: 15px; background: #f5f5f5; border: 1px solid #ddd;">
<div class="panel panel-default"> <h3 style="margin: 0 0 15px 0;">New Message</h3>
<div class="panel-heading">New Message</div> <form action="<?= Config::get('URL') ?>message/send" method="post" id="message-form">
<div class="panel-body"> <select name="receiver_id" style="width: 100%; padding: 8px; margin-bottom: 8px; border: 1px solid #ddd;" required>
<form action="<?= Config::get('URL') ?>message/send" method="post">
<div class="form-group">
<label for="receiver_id">To:</label>
<select name="receiver_id" id="receiver_id" class="form-control" 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>
</div> <input type="text" name="subject" placeholder="Subject" style="width: 100%; padding: 8px; margin-bottom: 8px; border: 1px solid #ddd; max-width: 250px;" required>
<div class="form-group"> <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>
<label for="subject">Subject:</label> <button type="submit" class="button">Send Message</button>
<input type="text" name="subject" id="subject" class="form-control" required>
</div>
<div class="form-group">
<label for="message">Message:</label>
<textarea name="message" id="message" class="form-control" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
</form> </form>
</div> </div>
</div> </div>
<!-- Send to Group --> <!-- Main Chat Area -->
<div class="panel panel-default"> <div style="flex: 1; border: 1px solid #ddd; background: white;" class="chat-main-area">
<div class="panel-heading">Send to Group</div> <div id="chat-content" style="height: 700px; min-height: 700px; display: flex; flex-direction: column;">
<div class="panel-body"> <!-- Default state -->
<form action="<?= Config::get('URL') ?>message/sendgroup" method="post"> <div id="default-state" style="flex: 1; display: flex; align-items: center; justify-content: center; background: #f9f9f9;">
<div class="form-group"> <div style="text-align: center; padding: 60px 20px; color: #666;">
<label for="group_type">Group:</label> <h3 style="margin: 0 0 10px 0;">Welcome to Messenger</h3>
<select name="group_type" id="group_type" class="form-control" required> <p>Select Global Chat or a conversation to start messaging</p>
<option value="">Select group</option> <small style="color: #999;">Click on any channel or conversation in the sidebar</small>
<option value="all_users">All Users</option>
<option value="admins">Administrators</option>
<option value="moderators">Moderators</option>
</select>
</div>
<div class="form-group">
<label for="group_subject">Subject:</label>
<input type="text" name="subject" id="group_subject" class="form-control" required>
</div>
<div class="form-group">
<label for="group_message">Message:</label>
<textarea name="message" id="group_message" class="form-control" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-warning">Send to Group</button>
</form>
</div>
</div> </div>
</div> </div>
<!-- Chat Area --> <!-- Chat interface (hidden by default) -->
<div class="col-md-8"> <div id="chat-interface" style="display: none; flex: 1; flex-direction: column;">
<div class="panel panel-default"> <!-- Chat header -->
<div class="panel-heading"> <div id="chat-header" style="padding: 15px; border-bottom: 1px solid #ddd; background: #f5f5f5; display: flex; justify-content: space-between; align-items: center;">
Chat Area <div id="chat-title"></div>
<span class="pull-right">Select a conversation to start messaging</span> <div id="chat-actions"></div>
</div>
<!-- Messages container -->
<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" style="padding: 15px; border-top: 1px solid #ddd; background: #f5f5f5;">
<!-- Message input will be loaded here -->
</div> </div>
<div class="panel-body" style="height: 500px; background-color: #f5f5f5; text-align: center; padding-top: 200px;">
<em>Select a conversation from the left to view messages</em>
</div> </div>
</div> </div>
</div> </div>
@@ -108,68 +101,601 @@
</div> </div>
<style> <style>
.message-container { .messaging-sidebar {
height: 400px; height: fit-content;
overflow-y: auto; }
background-color: #f5f5f5;
padding: 15px;
border-radius: 5px;
margin-bottom: 15px;
}
.message { .chat-main-area {
margin-bottom: 10px; height: auto;
clear: both; }
}
.message.sent { #messages-container {
text-align: right; background: #fafafa;
} scroll-behavior: smooth;
}
.message.received { .message-bubble {
text-align: left;
}
.message-bubble {
display: inline-block; display: inline-block;
max-width: 70%; max-width: 70%;
padding: 10px 15px; padding: 10px 15px;
border-radius: 18px; border-radius: 18px;
position: relative; margin-bottom: 8px;
} word-wrap: break-word;
line-height: 1.4;
}
.message.sent .message-bubble { .message-bubble.sent {
background-color: #007bff; background: #2196F3;
color: white; color: white;
float: right; border-bottom-right-radius: 4px;
}
.message-bubble.received {
background: white;
color: #333;
border: 1px solid #e0e0e0;
border-bottom-left-radius: 4px;
}
.message-time {
font-size: 11px;
color: #999;
margin-top: 4px;
}
.message-row {
margin-bottom: 15px;
display: flex;
}
.message-row.sent {
justify-content: flex-end;
}
.message-row.received {
justify-content: flex-start;
}
.global-message {
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.global-message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.global-message-bubble {
color: #333;
line-height: 1.5;
word-wrap: break-word;
}
@media (max-width: 768px) {
.chat-main-area {
height: 500px;
margin-top: 20px;
} }
.message.received .message-bubble { .message-bubble {
background-color: #e5e5ea; max-width: 85%;
color: black;
float: left;
}
.message-time {
font-size: 0.8em;
color: #666;
margin-top: 5px;
clear: both;
}
.badge {
background-color: #d9534f;
}
.list-group-item.active {
background-color: #d9edf7;
border-color: #bce8f1;
color: #31708f;
}
.list-group-item.active .badge {
background-color: #d9534f;
} }
}
</style> </style>
<script>
let currentChatType = null;
let currentChatId = null;
let messagePollingInterval = null;
// Function to scroll messages container to bottom
function scrollToBottom() {
const container = document.getElementById('messages-container');
if (container) {
// Small timeout to ensure content is rendered
setTimeout(() => {
container.scrollTop = container.scrollHeight;
}, 100);
}
}
// Handle DOM ready event
document.addEventListener('DOMContentLoaded', function() {
// Check URL hash for auto-loading chats
const hash = window.location.hash.substring(1);
if (hash === 'load-global') {
// Load global chat
setTimeout(() => loadGlobalChat(), 100);
} else if (hash.startsWith('load-conversation-')) {
// Load conversation
const userId = hash.split('-')[2];
if (userId) {
setTimeout(() => {
// Get user name from the conversation list
const conversationEl = document.querySelector('#conversation-' + userId);
if (conversationEl) {
const userName = conversationEl.querySelector('strong').textContent;
loadConversation(userId, userName);
}
}, 100);
}
}
});
function loadGlobalChat() {
currentChatType = 'global';
currentChatId = 'global';
// Hide default state, show chat interface
document.getElementById('default-state').style.display = 'none';
document.getElementById('chat-interface').style.display = 'flex';
// Update header
document.getElementById('chat-title').innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<span style="color: #2196F3; font-size: 18px;">#</span>
<strong>Global Chat</strong>
</div>
<small style="color: #666;">Public chatroom</small>
`;
document.getElementById('chat-actions').innerHTML = `
<span style="font-size: 12px; color: #666;">
<i class="fa fa-users"></i> Public
</span>
`;
// Highlight active chat
highlightActiveChat('global');
// Load messages
loadGlobalMessages();
// Scroll to bottom after loading
setTimeout(() => {
scrollToBottom();
}, 300);
// Setup message input
setupGlobalMessageInput();
// Start polling for new messages
startMessagePolling();
}
function loadConversation(userId, userName) {
currentChatType = 'conversation';
currentChatId = userId;
// Hide default state, show chat interface
document.getElementById('default-state').style.display = 'none';
document.getElementById('chat-interface').style.display = 'flex';
// Update header
document.getElementById('chat-title').innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<span style="color: #4CAF50; font-size: 18px;">@</span>
<strong>${userName}</strong>
</div>
<small style="color: #666;">Direct message</small>
`;
document.getElementById('chat-actions').innerHTML = `
<span style="font-size: 12px; color: #666;">
<i class="fa fa-user"></i> Private
</span>
`;
// Highlight active chat
highlightActiveChat(userId);
// Load messages
loadConversationMessages(userId);
// Scroll to bottom after loading
setTimeout(() => {
scrollToBottom();
}, 300);
// Setup message input
setupConversationMessageInput(userId);
// Start polling for new messages
startMessagePolling();
}
function highlightActiveChat(chatId) {
// Remove active class from all chats
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' ?
document.getElementById('global-chat-link') :
document.getElementById('conversation-' + chatId);
if (activeEl) {
activeEl.style.backgroundColor = '#e8f4fd';
activeEl.style.borderColor = '#2196F3';
}
}
function loadGlobalMessages() {
const container = document.getElementById('messages-container');
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #666;">Loading messages...</div>';
fetch('<?= Config::get('URL') ?>message/getGlobalMessages')
.then(response => {
console.log('Global messages response status:', 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();
})
.then(data => {
if (data.success && data.messages) {
displayGlobalMessages(data.messages);
} else {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Error loading messages: ' + (data.message || 'Unknown error') + '</div>';
}
})
.catch(error => {
console.error('Error loading global messages:', error);
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Network error: ' + error.message + '</div>';
});
}
function loadConversationMessages(userId) {
const container = document.getElementById('messages-container');
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #666;">Loading messages...</div>';
fetch(`<?= Config::get('URL') ?>message/getConversationMessages/${userId}`)
.then(response => response.json())
.then(data => {
if (data.success && data.messages) {
displayConversationMessages(data.messages);
} else {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Error loading messages</div>';
}
})
.catch(error => {
console.error('Error loading conversation messages:', error);
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #f44336;">Network error</div>';
});
}
function displayGlobalMessages(messages) {
const container = document.getElementById('messages-container');
if (messages.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 60px 20px;">
<h4 style="color: #666;">No messages yet</h4>
<p style="color: #999;">Start the conversation!</p>
</div>
`;
return;
}
container.innerHTML = messages.map(message => {
const timeString = new Date(message.created_at).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const isOwn = message.sender_id == document.querySelector('meta[name="user-id"]').content;
return `
<div class="global-message">
<div class="global-message-header">
<div style="display: flex; align-items: center; gap: 8px;">
<strong style="color: ${isOwn ? '#2196F3' : '#333'}">
${escapeHtml(message.sender_name)}
</strong>
${isOwn ? '<span style="font-size: 10px; background: #2196F3; color: white; padding: 2px 4px; border-radius: 3px;">You</span>' : ''}
</div>
<span class="message-time">${timeString}</span>
</div>
<div class="global-message-bubble">
${escapeHtml(message.message)}
</div>
</div>
`;
}).join('');
// Scroll to bottom
container.scrollTop = container.scrollHeight;
}
function displayConversationMessages(messages) {
const container = document.getElementById('messages-container');
const currentUserId = document.querySelector('meta[name="user-id"]').content;
if (messages.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 60px 20px;">
<h4 style="color: #666;">No messages yet</h4>
<p style="color: #999;">Send a message to start the conversation!</p>
</div>
`;
return;
}
container.innerHTML = messages.map(message => {
const isSent = message.sender_id == currentUserId;
const timeString = new Date(message.created_at).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
return `
<div class="message-row ${isSent ? 'sent' : 'received'}">
<div>
<div class="message-bubble ${isSent ? 'sent' : 'received'}">
${escapeHtml(message.message)}
</div>
<div class="message-time" style="text-align: ${isSent ? 'right' : 'left'}">
${timeString}
</div>
</div>
</div>
`;
}).join('');
// Scroll to bottom
container.scrollTop = container.scrollHeight;
}
function setupGlobalMessageInput() {
const inputArea = document.getElementById('message-input-area');
inputArea.innerHTML = `
<form id="global-message-form" onsubmit="sendGlobalMessage(event)">
<div style="display: flex; gap: 10px;">
<input type="text"
name="message"
placeholder="Type your global message..."
style="flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 25px;"
required>
<button type="submit" class="button" style="border-radius: 25px; padding: 12px 20px;">Send</button>
</div>
</form>
`;
}
function setupConversationMessageInput(userId) {
const inputArea = document.getElementById('message-input-area');
inputArea.innerHTML = `
<form id="conversation-message-form" onsubmit="sendConversationMessage(event, ${userId})">
<div style="display: flex; gap: 10px;">
<input type="text"
name="message"
placeholder="Type your message..."
style="flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 25px;"
required>
<button type="submit" class="button" style="border-radius: 25px; padding: 12px 20px;">Send</button>
</div>
</form>
`;
}
function sendGlobalMessage(event) {
event.preventDefault();
const form = event.target;
const message = form.message.value;
const submitButton = form.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.textContent = 'Sending...';
submitButton.disabled = true;
fetch('<?= Config::get('URL') ?>message/sendToGlobal', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: 'message=' + encodeURIComponent(message)
})
.then(response => {
console.log('Global message response status:', 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();
})
.then(data => {
if (data.success) {
form.reset();
loadGlobalMessages(); // Refresh messages
// Scroll to bottom after sending
setTimeout(() => {
scrollToBottom();
}, 500);
} else {
alert('Failed to send message: ' + (data.message || 'Unknown error'));
}
})
.catch(error => {
console.error('Error sending global message:', error);
alert('Network error. Please try again. (' + error.message + ')');
})
.finally(() => {
submitButton.textContent = originalText;
submitButton.disabled = false;
});
}
function sendConversationMessage(event, userId) {
event.preventDefault();
const form = event.target;
const message = form.message.value;
const submitButton = form.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.textContent = 'Sending...';
submitButton.disabled = true;
fetch('<?= Config::get('URL') ?>message/reply', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: 'receiver_id=' + userId + '&message=' + encodeURIComponent(message)
})
.then(response => {
console.log('Response status:', 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();
})
.then(data => {
if (data.success) {
form.reset();
loadConversationMessages(userId); // Refresh messages
// Scroll to bottom after sending
setTimeout(() => {
scrollToBottom();
}, 500);
} else {
alert('Failed to send message: ' + (data.message || 'Unknown error'));
}
})
.catch(error => {
console.error('Error sending conversation message:', error);
alert('Network error. Please try again. (' + error.message + ')');
})
.finally(() => {
submitButton.textContent = originalText;
submitButton.disabled = false;
});
}
function startMessagePolling() {
// Clear existing polling
if (messagePollingInterval) {
clearInterval(messagePollingInterval);
}
// Start new polling
messagePollingInterval = setInterval(() => {
if (currentChatType === 'global') {
loadGlobalMessages();
} else if (currentChatType === 'conversation') {
loadConversationMessages(currentChatId);
}
}, 3000); // Poll every 3 seconds
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Handle new message form submission
document.getElementById('message-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const submitButton = this.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.textContent = 'Sending...';
submitButton.disabled = true;
fetch(this.action, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
this.reset();
showFeedback('Message sent successfully!', 'success');
// Could refresh conversation list here
} else {
showFeedback(data.message || 'Failed to send message', 'error');
}
})
.catch(error => {
console.error('Error sending message:', error);
showFeedback('Network error. Please try again.', 'error');
})
.finally(() => {
submitButton.textContent = originalText;
submitButton.disabled = false;
});
});
function showFeedback(message, type) {
const feedback = document.createElement('div');
feedback.className = `feedback-${type}`;
feedback.textContent = message;
feedback.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background: ${type === 'success' ? '#4caf50' : '#f44336'};
color: white;
border-radius: 4px;
z-index: 1000;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
animation: slideIn 0.3s ease;
`;
document.body.appendChild(feedback);
setTimeout(() => {
feedback.style.animation = 'slideOut 0.3s ease';
setTimeout(() => feedback.remove(), 300);
}, 3000);
}
// Add CSS for animations
const animationStyle = document.createElement('style');
animationStyle.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(animationStyle);
</script>
<?php $this->render('_templates/footer'); ?> <?php $this->render('_templates/footer'); ?>

View File

@@ -1,44 +1,459 @@
<div class="container"> <div class="box">
<h1>NoteController/index</h1> <h1>My Notes</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?> <?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3> <div style="display: flex; gap: 20px; margin-top: 20px;">
<p> <!-- Notes Sidebar -->
This is just a simple CRUD implementation. Creating, reading, updating and deleting things. <div style="width: 300px;" class="notes-sidebar">
</p> <div style="padding: 15px; background: #f5f5f5; border: 1px solid #ddd; margin-bottom: 20px;">
<p> <form action="<?= Config::get('URL') ?>note/create" method="post" id="create-note-form" style="margin-bottom: 15px;">
<form method="post" action="<?php echo Config::get('URL');?>note/create"> <textarea name="note_text" placeholder="Write your note here..." style="width: 100%; max-width: 250px; padding: 8px; border: 1px solid #ddd; height: 80px; resize: vertical; margin-bottom: 10px;" required></textarea>
<label>Text of new note: </label><input type="text" name="note_text" /> <button type="submit" class="button">Create Note</button>
<input type="submit" value='Create this note' autocomplete="off" />
</form> </form>
</p> </div>
<?php if ($this->notes) { ?> <div style="padding: 15px; background: #f5f5f5; border: 1px solid #ddd;">
<table class="note-table"> <h3 style="margin: 0 0 15px 0;">Your Notes</h3>
<thead> <?php if ($this->notes): ?>
<tr> <?php foreach ($this->notes as $note): ?>
<td>Id</td> <div onclick="viewNote(<?= $note->note_id ?>)" style="padding: 12px; background: white; border: 1px solid #ddd; margin-bottom: 8px; cursor: pointer; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#f0f0f0'" onmouseout="this.style.backgroundColor='white'">
<td>Note</td> <div style="font-weight: bold; margin-bottom: 5px; word-wrap: break-word;">
<td>EDIT</td> <?= htmlspecialchars(substr($note->note_text, 0, 50)) ?><?= strlen($note->note_text) > 50 ? '...' : '' ?>
<td>DELETE</td> </div>
</tr> <div style="font-size: 12px; color: #666; margin-bottom: 8px;">
</thead> <?= date('M j, Y H:i', strtotime($note->note_timestamp)) ?>
<tbody> </div>
<?php foreach($this->notes as $key => $value) { ?> <div style="font-size: 11px; color: #999; font-style: italic; word-wrap: break-word;">
<tr> <?= htmlspecialchars(substr(strip_tags($note->note_text), 0, 80)) ?><?= strlen(strip_tags($note->note_text)) > 80 ? '...' : '' ?>
<td><?= $value->note_id; ?></td> </div>
<td><?= htmlentities($value->note_text); ?></td> <div style="display: flex; gap: 8px; margin-top: 8px;" onclick="event.stopPropagation()">
<td><a href="<?= Config::get('URL') . 'note/edit/' . $value->note_id; ?>">Edit</a></td> <a onclick="editNote(<?= $note->note_id ?>)" style="font-size: 12px; padding: 4px 8px; background: #4CAF50; color: white; text-decoration: none; border-radius: 3px;">Edit</a>
<td><a href="<?= Config::get('URL') . 'note/delete/' . $value->note_id; ?>">Delete</a></td> <a href="<?= Config::get('URL') ?>note/delete/<?= $note->note_id ?>" style="font-size: 12px; padding: 4px 8px; background: #f44336; color: white; text-decoration: none; border-radius: 3px;" onclick="return confirm('Are you sure you want to delete this note?');">Delete</a>
</tr> </div>
<?php } ?> </div>
</tbody> <?php endforeach; ?>
</table> <?php else: ?>
<?php } else { ?> <p style="text-align: center; padding: 20px; background: white; border: 1px solid #ddd;">No notes yet. Create your first note!</p>
<div>No notes yet. Create some !</div> <?php endif; ?>
<?php } ?> </div>
</div>
<!-- Notes Main Content -->
<div style="flex: 1; border: 1px solid #ddd; background: white;" class="note-main-content">
<div id="note-content" style="padding: 20px;">
<div style="text-align: center; padding: 60px 20px; color: #666; background: #f9f9f9;">
<h3>Note Viewer</h3>
<p>Select a note from the left to view its contents</p>
<small style="color: #999;">Click anywhere on a note card to view it</small>
</div>
</div>
<!-- Edit Note Form (Hidden by default) -->
<div id="note-edit-form" style="display: none; padding: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #ddd;">
<h3 style="margin: 0;">Editing Note</h3>
<div style="display: flex; gap: 8px;">
<button onclick="cancelEdit()" class="button" style="background: #757575; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">Cancel</button>
<button onclick="saveEdit()" class="button" style="background: #4CAF50; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">Save Changes</button>
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 8px; font-weight: bold; color: #333;">
Note Content (Markdown supported):
</label>
<textarea id="edit-note-text" style="width: 95%; padding: 12px; border: 1px solid #ddd; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.5; resize: vertical; min-height: 300px;" placeholder="Write your note here...
Markdown syntax supported:
# Header
**Bold** *italic* `code`
- List item
[Link](url)
> Quote"></textarea>
</div>
<div style="margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 4px;">
<h4 style="margin: 0 0 10px 0; color: #333;">Preview:</h4>
<div id="edit-preview" style="padding: 15px; background: white; border: 1px solid #ddd; border-radius: 4px; min-height: 100px; font-size: 14px; line-height: 1.6;"></div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<script>
let currentNoteId = null;
let isEditMode = false;
function viewNote(noteId) {
if (currentNoteId === noteId && !isEditMode) return; // Don't reload if same note and not in edit mode
currentNoteId = noteId;
isEditMode = false;
// Show view mode
document.getElementById('note-content').style.display = 'block';
document.getElementById('note-edit-form').style.display = 'none';
// Show loading state
const contentDiv = document.getElementById('note-content');
contentDiv.innerHTML = `
<div style="text-align: center; padding: 40px;">
<div style="display: inline-block; padding: 10px; background: #f0f0f0; border-radius: 4px;">
Loading note...
</div>
</div>
`;
// Load note via AJAX
fetch(`<?= Config::get('URL') ?>note/getNote/${noteId}`)
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
if (data.success) {
const note = data.note;
contentDiv.innerHTML = `
<div style="padding-bottom: 15px; border-bottom: 1px solid #ddd; margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;">
<div>
<h3 style="margin: 0; color: #333;">Note</h3>
<small style="color: #666;">
Created: ${new Date(note.note_timestamp).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</small>
</div>
<div style="display: flex; gap: 8px;">
<button onclick="editNote(${note.note_id})" style="padding: 8px 12px; background: #2196F3; color: white; border: none; border-radius: 4px; font-size: 12px; cursor: pointer;">Edit</button>
<a href="<?= Config::get('URL') ?>note/delete/${note.note_id}" style="padding: 8px 12px; background: #f44336; color: white; text-decoration: none; border-radius: 4px; font-size: 12px;" onclick="return confirm('Are you sure you want to delete this note?');">Delete</a>
</div>
</div>
<div style="white-space: pre-wrap; word-wrap: break-word; line-height: 1.6; font-size: 14px; color: #333;">
${note.note_html}
</div>
${note.note_text.length > 100 ? `
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; font-size: 12px; color: #666;">
<i class="fa fa-file-text-o"></i> ${note.note_text.length} characters
</div>
` : ''}
`;
// Highlight the active note in the sidebar
document.querySelectorAll('.notes-sidebar .note-card').forEach(card => {
card.style.backgroundColor = card.dataset.noteId == noteId ? '#e8f4fd' : 'white';
card.style.borderColor = card.dataset.noteId == noteId ? '#2196F3' : '#ddd';
});
} else {
contentDiv.innerHTML = `
<div style="text-align: center; padding: 40px; color: #f44336;">
<div style="font-size: 48px; margin-bottom: 10px;">⚠️</div>
<h4>Error loading note</h4>
<p>${data.message || 'Note not found'}</p>
</div>
`;
}
})
.catch(error => {
console.error('Error loading note:', error);
contentDiv.innerHTML = `
<div style="text-align: center; padding: 40px; color: #f44336;">
<div style="font-size: 48px; margin-bottom: 10px;">❌</div>
<h4>Network Error</h4>
<p>Could not load note. Please try again.</p>
</div>
`;
});
}
function editNote(noteId) {
isEditMode = true;
// Hide view mode, show edit form
document.getElementById('note-content').style.display = 'none';
document.getElementById('note-edit-form').style.display = 'block';
// Load the note content
fetch(`<?= Config::get('URL') ?>note/getNote/${noteId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const note = data.note;
const textarea = document.getElementById('edit-note-text');
const preview = document.getElementById('edit-preview');
textarea.value = note.note_text;
preview.innerHTML = note.note_html;
// Setup live preview
textarea.oninput = function() {
preview.innerHTML = SimpleMarkdown.parse(this.value);
};
}
})
.catch(error => {
console.error('Error loading note for editing:', error);
alert('Error loading note for editing');
});
}
function cancelEdit() {
isEditMode = false;
document.getElementById('note-content').style.display = 'block';
document.getElementById('note-edit-form').style.display = 'none';
viewNote(currentNoteId);
}
function saveEdit() {
const textarea = document.getElementById('edit-note-text');
const noteText = textarea.value;
if (!noteText.trim()) {
alert('Note content cannot be empty');
return;
}
const saveButton = document.querySelector('button[onclick="saveEdit()"]');
const originalText = saveButton.textContent;
saveButton.textContent = 'Saving...';
saveButton.disabled = true;
fetch(`<?= Config::get('URL') ?>note/editSave`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
note_id: currentNoteId,
note_text: noteText
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showFeedback('Note saved successfully!', 'success');
isEditMode = false;
document.getElementById('note-content').style.display = 'block';
document.getElementById('note-edit-form').style.display = 'none';
viewNote(currentNoteId); // Reload the note view
// Refresh the notes list to show the updated preview
setTimeout(() => {
refreshNotesList();
}, 500);
} else {
showFeedback(data.message || 'Failed to save note', 'error');
}
})
.catch(error => {
console.error('Error saving note:', error);
showFeedback('Network error. Please try again.', 'error');
})
.finally(() => {
saveButton.textContent = originalText;
saveButton.disabled = false;
});
}
// Simple markdown parser for browser use
class SimpleMarkdown {
static parse(text) {
text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// Headers
text = text.replace(/^#{1,6}\s+(.+)$/gm, '<h3>$1</h3>');
// Bold
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// Italic
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Code
text = text.replace(/`(.+?)`/g, '<code>$1</code>');
// Code blocks
text = text.replace(/```(.+?)```/gs, '<pre><code>$1</code></pre>');
// Links
text = text.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank">$1</a>');
// Blockquotes
text = text.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
// Lists
text = text.replace(/^\-\s+(.+)$/gm, '<li>$1</li>');
text = text.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>');
// Line breaks
text = text.replace(/\n/g, '<br>');
return text;
}
}
// Add note IDs to sidebar cards for highlighting
document.addEventListener('DOMContentLoaded', function() {
// Handle note creation form submission
const createForm = document.getElementById('create-note-form');
if (createForm) {
createForm.addEventListener('submit', function(e) {
e.preventDefault();
const noteText = this.note_text.value;
const submitButton = this.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
if (!noteText.trim()) {
alert('Please enter a note');
return;
}
// Show loading state
submitButton.textContent = 'Creating...';
submitButton.disabled = true;
fetch(`<?= Config::get('URL') ?>note/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({ note_text: noteText })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Clear form
this.reset();
// Show success message
showFeedback('Note created successfully!', 'success');
// Refresh notes list after a short delay
setTimeout(() => {
refreshNotesList();
}, 500);
} else {
showFeedback(data.message || 'Failed to create note', 'error');
}
})
.catch(error => {
console.error('Error creating note:', error);
showFeedback('Network error. Please try again.', 'error');
})
.finally(() => {
// Restore button state
submitButton.textContent = originalText;
submitButton.disabled = false;
});
});
}
// Add note IDs to sidebar cards for highlighting
const noteCards = document.querySelectorAll('.notes-sidebar > div:nth-child(2) > div');
noteCards.forEach((card, index) => {
// Skip the "Your Notes" header and the form
if (card.querySelector('a[href*="note/edit"]')) {
const editLink = card.querySelector('a[href*="note/edit"]');
const noteId = editLink.href.split('/').pop();
card.classList.add('note-card');
card.dataset.noteId = noteId;
card.style.cursor = 'pointer';
}
});
});
function refreshNotesList() {
// Reload the page to refresh the notes list
// This is simpler than trying to rebuild the sidebar via AJAX
window.location.reload();
}
function showFeedback(message, type) {
// Create feedback element
const feedback = document.createElement('div');
feedback.className = `feedback-${type}`;
feedback.textContent = message;
feedback.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background: ${type === 'success' ? '#4caf50' : '#f44336'};
color: white;
border-radius: 4px;
z-index: 1000;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
animation: slideIn 0.3s ease;
`;
document.body.appendChild(feedback);
// Remove after 3 seconds
setTimeout(() => {
feedback.style.animation = 'slideOut 0.3s ease';
setTimeout(() => feedback.remove(), 300);
}, 3000);
}
// Add CSS for animations
const animationStyle = document.createElement('style');
animationStyle.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.feedback-success {
background: linear-gradient(135deg, #4caf50, #45a049) !important;
}
.feedback-error {
background: linear-gradient(135deg, #f44336, #d32f2f) !important;
}
`;
document.head.appendChild(animationStyle);
// Add some CSS for better styling
const style = document.createElement('style');
style.textContent = `
.note-card {
transition: all 0.2s ease;
position: relative;
}
.note-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.note-card:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.note-main-content {
min-height: 500px;
}
@media (max-width: 768px) {
.note-main-content {
margin-top: 20px;
}
}
`;
document.head.appendChild(style);
</script>

View File

@@ -0,0 +1,12 @@
<svg width="600" height="75" viewBox="0 0 600 75" version="1.1" xmlns="http://www.w3.org/2000/svg" style="stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 1.5;">
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M105.809,48.397C105.809,44.506 102.473,43.931 102.473,33.503" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M109.397,38.324L109.397,48.321" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M112.883,48.152C112.883,44.717 115.053,40.554 115.053,35.084C115.053,29.613 114.393,24.795 114.216,21.81" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M112.951,22.241C112.951,22.241 116.335,21.976 117.504,16.695" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M107.788,11.843C107.788,11.843 106.369,7.434 105.169,7.434C103.969,7.434 101.87,13.187 101.87,21.862C101.87,24.103 90.181,29.985 92.659,43.571C93.057,45.751 94.053,49.908 94.053,49.924C94.053,49.94 96.571,59.453 91.184,59.453C90.063,59.453 89.526,58.833 88.405,58.833C87.285,58.833 86.381,59.598 86.381,60.591C86.381,61.584 87.491,64.025 91.446,64.025C98.593,64.025 98.865,58.038 98.865,54.158C98.865,50.278 98.829,51.479 98.829,50.844C98.829,48.717 100.601,48.284 101.259,48.043" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
<ellipse transform="matrix(1.00474,-0.404483,0.370766,0.920982,85.4108,49.8267)" cx="111.892" cy="15.766" rx="1.032" ry="1.449" style="fill: rgb(47, 44, 62);"/>
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M110.074,10.347C113.617,10.347 114.448,14.635 117.14,14.635" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
<path transform="matrix(1,0,0,1,92.3579,4.11772)" d="M112.568,9.074C112.568,9.074 111.553,6.74 110.677,6.74C109.801,6.74 108.537,9.169 108.537,9.169" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 1.5px;"/>
<path transform="matrix(3.96613,0,0,5.89452,-177.012,-336.835)" d="M93.717,66.428L195.647,66.428" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 0.3px;"/>
<path transform="matrix(1.78906,0,0,2.78204,-166.7,-130.078)" d="M93.717,66.428L195.647,66.428" style="fill: none; stroke: rgb(110, 108, 126); stroke-width: 0.64px;"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

33
migration_add_is_read.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
/**
* Migration: Add is_read column to messages table
*/
// Include database connection
require_once __DIR__ . '/../../config.php';
try {
$database = DatabaseFactory::getFactory()->getConnection();
// Check if is_read column exists
$sql = "SHOW COLUMNS FROM messages LIKE 'is_read'";
$query = $database->prepare($sql);
$query->execute();
$result = $query->fetch();
if (!$result) {
// Add is_read column
$sql = "ALTER TABLE messages ADD COLUMN is_read TINYINT(1) NOT NULL DEFAULT 0";
$query = $database->prepare($sql);
$query->execute();
echo "Successfully added is_read column to messages table\n";
} else {
echo "is_read column already exists in messages table\n";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>

View File

@@ -85,9 +85,12 @@ body {
} }
/* TODO */ /* TODO */
.navigation { .navigation {
} }
.navigation.right { .navigation.right {
float: right; float: right;
position: relative;
z-index: 99;
} }
.navigation li { .navigation li {
float: left; float: left;

293
public/js/messaging.js Normal file
View File

@@ -0,0 +1,293 @@
// Messaging JavaScript functionality
class MessagingSystem {
constructor() {
this.currentUserId = null;
this.currentConversationUserId = null;
this.pollingInterval = null;
this.init();
}
init() {
// Get current user ID from the page
this.currentUserId = document.querySelector('meta[name="user-id"]')?.content;
if (this.currentUserId) {
this.setupEventListeners();
this.startPolling();
}
}
setupEventListeners() {
// Handle message form submissions
document.querySelectorAll('form[id$="-form"], form[action*="message/send"]').forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
this.sendMessage(form);
});
});
// Handle conversation switching
document.querySelectorAll('a[href*="message/conversation/"]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const userId = link.href.split('/').pop();
this.loadConversation(userId);
});
});
}
async sendMessage(form) {
try {
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// Show loading state
const submitButton = form.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.textContent = 'Sending...';
submitButton.disabled = true;
const response = await fetch('index.php/message/send', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams(data)
});
const result = await response.json();
if (result.success) {
// Clear form
form.reset();
// Reload messages or show success
if (this.currentConversationUserId) {
await this.loadMessages(this.currentConversationUserId);
} else {
this.showFeedback('Message sent successfully!', 'success');
}
} else {
this.showFeedback(result.message || 'Failed to send message', 'error');
}
} catch (error) {
console.error('Error sending message:', error);
this.showFeedback('Network error. Please try again.', 'error');
} finally {
// Restore button state
const submitButton = form.querySelector('button[type="submit"]');
submitButton.textContent = originalText;
submitButton.disabled = false;
}
}
async loadConversation(userId) {
this.currentConversationUserId = userId;
try {
const response = await fetch(`index.php/message/conversation/${userId}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const html = await response.text();
const conversationContainer = document.querySelector('.conversation-container') ||
document.querySelector('.panel-default');
if (conversationContainer) {
conversationContainer.innerHTML = html;
this.setupEventListeners(); // Re-setup event listeners for new content
}
}
} catch (error) {
console.error('Error loading conversation:', error);
this.showFeedback('Failed to load conversation', 'error');
}
}
async loadMessages(userId) {
try {
const response = await fetch(`index.php/message/getConversationMessages/${userId}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const messages = await response.json();
this.displayMessages(messages);
}
} catch (error) {
console.error('Error loading messages:', error);
}
}
async loadGlobalMessages() {
try {
const response = await fetch('index.php/message/getGlobalMessages', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const messages = await response.json();
this.displayGlobalMessages(messages);
}
} catch (error) {
console.error('Error loading global messages:', error);
}
}
displayMessages(messages) {
const messageContainer = document.querySelector('.message-container');
if (!messageContainer) return;
messageContainer.innerHTML = '';
messages.forEach(message => {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${message.sender_id == this.currentUserId ? 'sent' : 'received'}`;
messageDiv.innerHTML = `
<div class="message-bubble">
${this.escapeHtml(message.message)}
</div>
<div class="message-time">
${this.formatDate(message.created_at)}
</div>
`;
messageContainer.appendChild(messageDiv);
});
// Scroll to bottom
messageContainer.scrollTop = messageContainer.scrollHeight;
}
displayGlobalMessages(messages) {
const globalContainer = document.querySelector('.global-messages-container');
if (!globalContainer) return;
globalContainer.innerHTML = '';
messages.forEach(message => {
const messageDiv = document.createElement('div');
messageDiv.className = 'global-message';
messageDiv.innerHTML = `
<div class="message-header">
<strong>${this.escapeHtml(message.sender_name)}</strong>
<span class="message-time">${this.formatDate(message.created_at)}</span>
</div>
<div class="message-bubble">
${this.escapeHtml(message.message)}
</div>
`;
globalContainer.appendChild(messageDiv);
});
// Scroll to bottom
globalContainer.scrollTop = globalContainer.scrollHeight;
}
startPolling() {
// Poll for new messages every 5 seconds
this.pollingInterval = setInterval(() => {
this.checkForNewMessages();
}, 5000);
// Also check for unread count
setInterval(() => {
this.updateUnreadCount();
}, 10000);
}
async checkForNewMessages() {
if (this.currentConversationUserId) {
await this.loadMessages(this.currentConversationUserId);
}
}
async updateUnreadCount() {
try {
const response = await fetch('index.php/message/unreadcount', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const data = await response.json();
this.updateUnreadCountDisplay(data.count);
}
} catch (error) {
console.error('Error updating unread count:', error);
}
}
updateUnreadCountDisplay(count) {
const unreadElements = document.querySelectorAll('.unread-count');
unreadElements.forEach(element => {
if (count > 0) {
element.textContent = count;
element.style.display = 'inline-block';
} else {
element.style.display = 'none';
}
});
}
showFeedback(message, type) {
// Create feedback element
const feedback = document.createElement('div');
feedback.className = `feedback-${type}`;
feedback.textContent = message;
feedback.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background: ${type === 'success' ? '#4caf50' : '#f44336'};
color: white;
border-radius: 4px;
z-index: 1000;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
`;
document.body.appendChild(feedback);
// Remove after 3 seconds
setTimeout(() => {
feedback.remove();
}, 3000);
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
}
// Initialize the messaging system when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.messagingSystem = new MessagingSystem();
});
// Make it available globally for any other scripts that might need it
window.MessagingSystem = MessagingSystem;

118
test_messaging.php Normal file
View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Messaging Endpoints</title>
</head>
<body>
<h1>Test Messaging Endpoints</h1>
<h2>Test 1: Get Global Messages</h2>
<button onclick="testGetGlobalMessages()">Test Get Global Messages</button>
<div id="result1"></div>
<h2>Test 2: Get Conversation Messages</h2>
<button onclick="testGetConversationMessages()">Test Get Conversation Messages</button>
<div id="result2"></div>
<h2>Test 3: Send Global Message</h2>
<button onclick="testSendGlobalMessage()">Test Send Global Message</button>
<div id="result3"></div>
<h2>Test 4: Send Conversation Message</h2>
<button onclick="testSendConversationMessage()">Test Send Conversation Message</button>
<div id="result4"></div>
<script>
async function testGetGlobalMessages() {
const resultDiv = document.getElementById('result1');
resultDiv.innerHTML = '<p>Testing...</p>';
try {
const response = await fetch('<?= Config::get('URL') ?>message/getGlobalMessages');
const text = await response.text();
resultDiv.innerHTML = `
<p><strong>Status:</strong> ${response.status}</p>
<p><strong>Headers:</strong> ${response.headers.get('content-type')}</p>
<p><strong>Response:</strong></p>
<pre style="background: #f0f0f0; padding: 10px; overflow-x: auto;">${text}</pre>
`;
} catch (error) {
resultDiv.innerHTML = `<p style="color: red;">Error: ${error.message}</p>`;
}
}
async function testGetConversationMessages() {
const resultDiv = document.getElementById('result2');
resultDiv.innerHTML = '<p>Testing...</p>';
try {
// Try to get user ID from conversation (first user)
const response = await fetch('<?= Config::get('URL') ?>message/getConversationMessages/1');
const text = await response.text();
resultDiv.innerHTML = `
<p><strong>Status:</strong> ${response.status}</p>
<p><strong>Headers:</strong> ${response.headers.get('content-type')}</p>
<p><strong>Response:</strong></p>
<pre style="background: #f0f0f0; padding: 10px; overflow-x: auto;">${text}</pre>
`;
} catch (error) {
resultDiv.innerHTML = `<p style="color: red;">Error: ${error.message}</p>`;
}
}
async function testSendGlobalMessage() {
const resultDiv = document.getElementById('result3');
resultDiv.innerHTML = '<p>Testing...</p>';
try {
const response = await fetch('<?= Config::get('URL') ?>message/sendToGlobal', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: 'message=Test message from test script'
});
const text = await response.text();
resultDiv.innerHTML = `
<p><strong>Status:</strong> ${response.status}</p>
<p><strong>Headers:</strong> ${response.headers.get('content-type')}</p>
<p><strong>Response:</strong></p>
<pre style="background: #f0f0f0; padding: 10px; overflow-x: auto;">${text}</pre>
`;
} catch (error) {
resultDiv.innerHTML = `<p style="color: red;">Error: ${error.message}</p>`;
}
}
async function testSendConversationMessage() {
const resultDiv = document.getElementById('result4');
resultDiv.innerHTML = '<p>Testing...</p>';
try {
const response = await fetch('<?= Config::get('URL') ?>message/reply', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: 'receiver_id=1&message=Test message from test script'
});
const text = await response.text();
resultDiv.innerHTML = `
<p><strong>Status:</strong> ${response.status}</p>
<p><strong>Headers:</strong> ${response.headers.get('content-type')}</p>
<p><strong>Response:</strong></p>
<pre style="background: #f0f0f0; padding: 10px; overflow-x: auto;">${text}</pre>
`;
} catch (error) {
resultDiv.innerHTML = `<p style="color: red;">Error: ${error.message}</p>`;
}
}
</script>
</body>
</html>