Introduction to the Project
The Notes Taking App with Database is a comprehensive, full-stack web application designed to help users capture, organize, and manage their notes efficiently. This system provides a robust platform for personal note-taking, task management, idea organization, and information retention. With user authentication, rich text editing, categorization, and search capabilities, it offers a complete solution for digital note management.
The application features role-based access control with three user types: Admin, Premium Users, and Basic Users. Notes are stored securely in a database with support for rich text formatting, attachments, tags, categories, and sharing options. Whether you're a student taking lecture notes, a professional organizing project ideas, or a writer collecting research, this notes app provides all the essential tools for effective note management.
Key Features
Core Features
- User Authentication: Secure registration and login system
- Note Creation: Create notes with titles and rich text content
- Rich Text Editor: Format text with bold, italic, lists, headings, and more
- Categories: Organize notes by custom categories
- Tags: Add multiple tags to notes for easy filtering
- Search: Full-text search across all notes
- Favorites: Mark important notes as favorites
- Archive: Archive old notes instead of deleting
- Trash: Recover deleted notes from trash
- Responsive Design: Works on desktop, tablet, and mobile
Advanced Features
- Rich Text Editing:
- Bold, italic, underline, strikethrough
- Headings (H1, H2, H3)
- Lists (ordered and unordered)
- Links and images
- Code blocks
- Tables
- Blockquotes
- Horizontal rules
- Text color and highlighting
- Undo/redo functionality
- Organization:
- Nested categories/folders
- Color-coded categories
- Drag-and-drop organization
- Pin important notes
- Custom sorting options
- Grid and list views
- Search & Filter:
- Full-text search
- Filter by category
- Filter by tags
- Filter by date range
- Filter by favorites
- Filter by archive status
- Saved searches
- Collaboration:
- Share notes with other users
- Public/private note visibility
- Comment on shared notes
- Collaborative editing
- Version history
- Note locking
- Export & Import:
- Export notes as PDF
- Export as Markdown
- Export as plain text
- Export as HTML
- Bulk export
- Import from Markdown
- Import from plain text
- Attachments:
- Upload images to notes
- Attach files (PDF, DOC, etc.)
- Image gallery
- File management
- Storage quota tracking
- Reminders:
- Set reminders on notes
- Due dates for tasks
- Email notifications
- Push notifications
- Recurring reminders
- Templates:
- Save note templates
- Quick note creation
- Meeting notes template
- To-do list template
- Journal template
- Premium Features:
- Unlimited storage
- Advanced formatting
- Version history (30 days)
- Collaborative editing
- Priority support
- No ads
- Advanced export options
Admin Features
- User Management: View, edit, suspend, or delete users
- Storage Monitoring: Track storage usage per user
- System Settings: Configure application parameters
- Analytics: View usage statistics
- Backup Management: Database backup and restore
- Logs: View system and error logs
Technology Stack
- Frontend: HTML5, CSS3, JavaScript (ES6+), Bootstrap 5
- Rich Text Editor: TinyMCE or CKEditor 5
- Backend: PHP 8.0+ (Core PHP with OOP approach)
- Database: MySQL 5.7+ or MariaDB
- Additional Libraries:
- Bootstrap 5 for responsive UI
- Font Awesome for icons
- jQuery for AJAX operations
- Select2 for enhanced dropdowns
- DataTables for advanced tables
- Moment.js for date handling
- TinyMCE for rich text editing
- PHPMailer for email notifications
- TCPDF for PDF export
- Parsedown for Markdown processing
- Intervention Image for image handling
Project File Structure
notes-app/ │ ├── assets/ │ ├── css/ │ │ ├── style.css │ │ ├── dashboard.css │ │ ├── editor.css │ │ ├── dark-mode.css │ │ └── responsive.css │ ├── js/ │ │ ├── main.js │ │ ├── dashboard.js │ │ ├── editor.js │ │ ├── notes.js │ │ ├── search.js │ │ ├── validation.js │ │ └── tinymce-init.js │ ├── images/ │ │ ├── avatars/ │ │ ├── note-images/ │ │ └── attachments/ │ └── plugins/ │ ├── tinymce/ │ ├── datatables/ │ └── select2/ │ ├── includes/ │ ├── config.php │ ├── Database.php │ ├── functions.php │ ├── auth.php │ ├── Note.php │ ├── Category.php │ ├── Tag.php │ ├── User.php │ ├── Attachment.php │ ├── Share.php │ ├── Reminder.php │ ├── Template.php │ └── helpers/ │ ├── TextHelper.php │ ├── FileHelper.php │ └── ExportHelper.php │ ├── admin/ │ ├── dashboard.php │ ├── manage_users.php │ ├── user_details.php │ ├── system_settings.php │ ├── storage_stats.php │ ├── backup.php │ ├── logs.php │ └── analytics.php │ ├── user/ │ ├── dashboard.php │ ├── notes.php │ ├── create_note.php │ ├── edit_note.php │ ├── view_note.php │ ├── categories.php │ ├── create_category.php │ ├── edit_category.php │ ├── tags.php │ ├── favorites.php │ ├── archive.php │ ├── trash.php │ ├── search.php │ ├── attachments.php │ ├── reminders.php │ ├── templates.php │ ├── shared_with_me.php │ ├── profile.php │ ├── settings.php │ └── upgrade.php │ ├── api/ │ ├── get_notes.php │ ├── save_note.php │ ├── delete_note.php │ ├── restore_note.php │ ├── search_notes.php │ ├── get_categories.php │ ├── get_tags.php │ ├── upload_attachment.php │ ├── share_note.php │ └── set_reminder.php │ ├── uploads/ │ ├── avatars/ │ ├── note-images/ │ └── attachments/ │ ├── documents/ │ └── images/ │ ├── vendor/ │ ├── index.php ├── login.php ├── register.php ├── forgot_password.php ├── reset_password.php ├── logout.php ├── verify.php ├── .env ├── .gitignore ├── composer.json └── sql/ └── database.sql
Database Schema
File: sql/database.sql
-- Create Database
CREATE DATABASE IF NOT EXISTS `notes_app`;
USE `notes_app`;
-- Users Table
CREATE TABLE `users` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) UNIQUE NOT NULL,
`email` VARCHAR(100) UNIQUE NOT NULL,
`password` VARCHAR(255) NOT NULL,
`first_name` VARCHAR(50) NOT NULL,
`last_name` VARCHAR(50) NOT NULL,
`display_name` VARCHAR(100),
`bio` TEXT,
`profile_picture` VARCHAR(255) DEFAULT 'default.png',
`role` ENUM('admin', 'premium', 'basic') DEFAULT 'basic',
`status` ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
-- Account Settings
`email_verified` BOOLEAN DEFAULT FALSE,
`verification_token` VARCHAR(255),
`reset_token` VARCHAR(255),
`reset_expires` DATETIME,
-- Preferences
`theme` ENUM('light', 'dark', 'auto') DEFAULT 'light',
`default_view` ENUM('grid', 'list') DEFAULT 'grid',
`items_per_page` INT DEFAULT 20,
`editor_type` ENUM('simple', 'full') DEFAULT 'full',
-- Storage
`storage_used` BIGINT DEFAULT 0,
`storage_limit` BIGINT DEFAULT 104857600, -- 100 MB for basic, 1GB for premium
`notes_count` INT DEFAULT 0,
-- Premium Features
`premium_until` DATETIME,
`last_backup` DATETIME,
-- Timestamps
`last_login` DATETIME,
`last_ip` VARCHAR(45),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_email` (`email`),
INDEX `idx_username` (`username`),
INDEX `idx_role` (`role`),
INDEX `idx_status` (`status`)
);
-- Categories Table (Folders)
CREATE TABLE `categories` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`slug` VARCHAR(100) NOT NULL,
`description` TEXT,
`color` VARCHAR(7) DEFAULT '#6c757d',
`icon` VARCHAR(50) DEFAULT 'fa-folder',
`parent_id` INT(11) DEFAULT NULL,
`sort_order` INT DEFAULT 0,
`is_system` BOOLEAN DEFAULT FALSE,
`notes_count` INT DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`parent_id`) REFERENCES `categories`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_category_per_user` (`user_id`, `slug`),
INDEX `idx_user` (`user_id`)
);
-- Tags Table
CREATE TABLE `tags` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`name` VARCHAR(50) NOT NULL,
`slug` VARCHAR(50) NOT NULL,
`color` VARCHAR(7) DEFAULT '#6c757d',
`notes_count` INT DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_tag_per_user` (`user_id`, `slug`),
INDEX `idx_user` (`user_id`)
);
-- Notes Table
CREATE TABLE `notes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`category_id` INT(11) DEFAULT NULL,
`uuid` VARCHAR(36) UNIQUE NOT NULL,
`title` VARCHAR(255) NOT NULL,
`slug` VARCHAR(255) NOT NULL,
`content` LONGTEXT,
`content_plain` TEXT,
`excerpt` TEXT,
-- Formatting
`editor_type` ENUM('plain', 'rich', 'markdown') DEFAULT 'rich',
-- Status
`is_favorite` BOOLEAN DEFAULT FALSE,
`is_archived` BOOLEAN DEFAULT FALSE,
`is_trashed` BOOLEAN DEFAULT FALSE,
`is_public` BOOLEAN DEFAULT FALSE,
`is_pinned` BOOLEAN DEFAULT FALSE,
`is_protected` BOOLEAN DEFAULT FALSE,
`password` VARCHAR(255),
-- Metadata
`color` VARCHAR(7) DEFAULT NULL,
`icon` VARCHAR(50) DEFAULT NULL,
`cover_image` VARCHAR(255),
-- Statistics
`views` INT DEFAULT 0,
`word_count` INT DEFAULT 0,
`character_count` INT DEFAULT 0,
`reading_time` INT DEFAULT 0, -- in minutes
-- Versioning
`version` INT DEFAULT 1,
-- Timestamps
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON DELETE SET NULL,
UNIQUE KEY `unique_slug_per_user` (`user_id`, `slug`),
INDEX `idx_user` (`user_id`),
INDEX `idx_category` (`category_id`),
INDEX `idx_favorite` (`is_favorite`),
INDEX `idx_archived` (`is_archived`),
INDEX `idx_trashed` (`is_trashed`),
INDEX `idx_public` (`is_public`),
INDEX `idx_pinned` (`is_pinned`),
INDEX `idx_created` (`created_at`),
INDEX `idx_updated` (`updated_at`),
FULLTEXT INDEX `idx_search` (`title`, `content`, `content_plain`, `excerpt`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Note Tags Junction Table
CREATE TABLE `note_tags` (
`note_id` INT(11) NOT NULL,
`tag_id` INT(11) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`note_id`, `tag_id`),
FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON DELETE CASCADE
);
-- Attachments Table
CREATE TABLE `attachments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`note_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`filename` VARCHAR(255) NOT NULL,
`original_filename` VARCHAR(255) NOT NULL,
`file_path` VARCHAR(255) NOT NULL,
`file_size` INT NOT NULL,
`file_type` VARCHAR(100),
`mime_type` VARCHAR(100),
`width` INT DEFAULT NULL,
`height` INT DEFAULT NULL,
`is_image` BOOLEAN DEFAULT FALSE,
`download_count` INT DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_note` (`note_id`),
INDEX `idx_user` (`user_id`)
);
-- Note Versions Table (for premium users)
CREATE TABLE `note_versions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`note_id` INT(11) NOT NULL,
`version` INT NOT NULL,
`title` VARCHAR(255) NOT NULL,
`content` LONGTEXT,
`word_count` INT DEFAULT 0,
`character_count` INT DEFAULT 0,
`created_by` INT(11),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE SET NULL,
INDEX `idx_note` (`note_id`),
UNIQUE KEY `unique_note_version` (`note_id`, `version`)
);
-- Shared Notes Table
CREATE TABLE `shared_notes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`note_id` INT(11) NOT NULL,
`shared_by` INT(11) NOT NULL,
`shared_with` INT(11),
`share_token` VARCHAR(64) UNIQUE,
`email` VARCHAR(100),
`permission` ENUM('view', 'comment', 'edit') DEFAULT 'view',
`expires_at` DATETIME,
`view_count` INT DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`shared_by`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`shared_with`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_note` (`note_id`),
INDEX `idx_token` (`share_token`),
INDEX `idx_email` (`email`)
);
-- Comments on Shared Notes
CREATE TABLE `comments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`note_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`parent_id` INT(11) DEFAULT NULL,
`content` TEXT NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`parent_id`) REFERENCES `comments`(`id`) ON DELETE CASCADE,
INDEX `idx_note` (`note_id`)
);
-- Reminders Table
CREATE TABLE `reminders` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`note_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`title` VARCHAR(255),
`reminder_time` DATETIME NOT NULL,
`repeat_type` ENUM('none', 'daily', 'weekly', 'monthly', 'yearly') DEFAULT 'none',
`repeat_interval` INT DEFAULT 1,
`repeat_end` DATETIME,
`is_completed` BOOLEAN DEFAULT FALSE,
`completed_at` DATETIME,
`notification_sent` BOOLEAN DEFAULT FALSE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`note_id`) REFERENCES `notes`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_user` (`user_id`),
INDEX `idx_time` (`reminder_time`),
INDEX `idx_completed` (`is_completed`)
);
-- Templates Table
CREATE TABLE `templates` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`description` TEXT,
`content` LONGTEXT NOT NULL,
`category` VARCHAR(50) DEFAULT 'general',
`is_system` BOOLEAN DEFAULT FALSE,
`usage_count` INT DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_user` (`user_id`)
);
-- System Templates (predefined)
INSERT INTO `templates` (`user_id`, `name`, `description`, `content`, `category`, `is_system`) VALUES
(1, 'Meeting Notes', 'Template for taking meeting notes', '<h1>Meeting Notes</h1>\n\n<h2>Date:</h2>\n\n<h2>Attendees:</h2>\n\n<h2>Agenda:</h2>\n\n<ol>\n<li></li>\n</ol>\n\n<h2>Discussion Points:</h2>\n\n<h2>Action Items:</h2>\n\n<table>\n<thead>\n<tr>\n<th>Task</th>\n<th>Owner</th>\n<th>Due Date</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td></td>\n<td></td>\n<td></td>\n</tr>\n</tbody>\n</table>\n\n<h2>Next Meeting:</h2>', 'meeting', 1),
(1, 'To-Do List', 'Simple to-do list template', '<h1>To-Do List</h1>\n\n<ul>\n<li>[ ] Task 1</li>\n<li>[ ] Task 2</li>\n<li>[ ] Task 3</li>\n</ul>\n\n<h2>Priority:</h2>\n\n<ol>\n<li>High priority task</li>\n<li>Medium priority task</li>\n<li>Low priority task</li>\n</ol>', 'task', 1),
(1, 'Journal Entry', 'Daily journal template', '<h1>Journal Entry</h1>\n\n<h2>Date:</h2>\n\n<h2>Mood:</h2>\n\n<h2>Gratitude:</h2>\n\n<ul>\n<li></li>\n</ul>\n\n<h2>Today''s Highlights:</h2>\n\n<h2>Challenges:</h2>\n\n<h2>Tomorrow''s Goals:</h2>', 'journal', 1),
(1, 'Project Plan', 'Project planning template', '<h1>Project Plan</h1>\n\n<h2>Project Name:</h2>\n\n<h2>Objective:</h2>\n\n<h2>Timeline:</h2>\n\n<h2>Milestones:</h2>\n\n<ol>\n<li></li>\n</ol>\n\n<h2>Tasks:</h2>\n\n<table>\n<thead>\n<tr>\n<th>Task</th>\n<th>Assigned To</th>\n<th>Deadline</th>\n<th>Status</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n</tr>\n</tbody>\n</table>\n\n<h2>Resources:</h2>\n\n<h2>Notes:</h2>', 'project', 1),
(1, 'Recipe', 'Recipe template', '<h1>Recipe: </h1>\n\n<h2>Prep Time:</h2>\n\n<h2>Cook Time:</h2>\n\n<h2>Servings:</h2>\n\n<h2>Ingredients:</h2>\n\n<ul>\n<li></li>\n</ul>\n\n<h2>Instructions:</h2>\n\n<ol>\n<li></li>\n</ol>\n\n<h2>Notes:</h2>', 'food', 1);
-- User Settings Table
CREATE TABLE `user_settings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`setting_key` VARCHAR(100) NOT NULL,
`setting_value` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_setting` (`user_id`, `setting_key`)
);
-- Notifications Table
CREATE TABLE `notifications` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`type` ENUM('reminder', 'share', 'comment', 'system', 'upgrade') NOT NULL,
`title` VARCHAR(255) NOT NULL,
`message` TEXT NOT NULL,
`data` JSON,
`is_read` BOOLEAN DEFAULT FALSE,
`read_at` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_user_read` (`user_id`, `is_read`)
);
-- System Settings
CREATE TABLE `system_settings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`setting_key` VARCHAR(100) UNIQUE NOT NULL,
`setting_value` TEXT,
`description` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
-- Insert Default Admin
INSERT INTO `users` (`username`, `email`, `password`, `first_name`, `last_name`, `role`, `email_verified`, `storage_limit`)
VALUES ('admin', '[email protected]', '$2y$10$YourHashedPasswordHere', 'System', 'Administrator', 'admin', TRUE, 1073741824); -- 1GB for admin
-- Insert Default System Settings
INSERT INTO `system_settings` (`setting_key`, `setting_value`, `description`) VALUES
('site_name', 'Notes App', 'Name of the site'),
('site_description', 'Your personal notes taking application', 'Site description'),
('site_keywords', 'notes, notepad, organizer, productivity', 'SEO keywords'),
('site_email', '[email protected]', 'Contact email'),
('allow_registration', '1', 'Allow user registration'),
('require_email_verification', '1', 'Require email verification'),
('default_user_role', 'basic', 'Default role for new users'),
('basic_storage_limit', '104857600', 'Storage limit for basic users (100 MB)'),
('premium_storage_limit', '1073741824', 'Storage limit for premium users (1 GB)'),
('max_file_size', '5242880', 'Maximum file size for uploads (5 MB)'),
('allowed_file_types', 'jpg,jpeg,png,gif,pdf,doc,docx,txt,md', 'Allowed file types'),
('enable_rich_editor', '1', 'Enable rich text editor'),
('enable_markdown', '1', 'Enable Markdown support'),
('enable_attachments', '1', 'Enable file attachments'),
('enable_sharing', '1', 'Enable note sharing'),
('enable_reminders', '1', 'Enable reminders'),
('items_per_page', '20', 'Default items per page'),
('timezone', 'America/New_York', 'Default timezone'),
('date_format', 'Y-m-d', 'Date format'),
('time_format', 'H:i', 'Time format');
Core PHP Classes
Database Class
File: includes/Database.php
<?php
/**
* Database Class
* Handles all database connections and operations using PDO with singleton pattern
*/
class Database {
private static $instance = null;
private $connection;
private $statement;
private $host;
private $dbname;
private $username;
private $password;
/**
* Private constructor for singleton pattern
*/
private function __construct() {
$this->host = DB_HOST;
$this->dbname = DB_NAME;
$this->username = DB_USER;
$this->password = DB_PASS;
try {
$this->connection = new PDO(
"mysql:host={$this->host};dbname={$this->dbname};charset=utf8mb4",
$this->username,
$this->password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
/**
* Get database instance (Singleton)
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Prepare and execute query with parameters
*/
public function query($sql, $params = []) {
try {
$this->statement = $this->connection->prepare($sql);
$this->statement->execute($params);
return $this->statement;
} catch (PDOException $e) {
$this->logError($e->getMessage(), $sql, $params);
throw new Exception("Database query failed: " . $e->getMessage());
}
}
/**
* Get single row
*/
public function getRow($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetch();
}
/**
* Get multiple rows
*/
public function getRows($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetchAll();
}
/**
* Get single value
*/
public function getValue($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetchColumn();
}
/**
* Insert data and return last insert ID
*/
public function insert($table, $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$this->query($sql, $data);
return $this->connection->lastInsertId();
}
/**
* Update data
*/
public function update($table, $data, $where, $whereParams = []) {
$set = [];
foreach (array_keys($data) as $column) {
$set[] = "{$column} = :{$column}";
}
$sql = "UPDATE {$table} SET " . implode(', ', $set) . " WHERE {$where}";
$params = array_merge($data, $whereParams);
return $this->query($sql, $params)->rowCount();
}
/**
* Delete data
*/
public function delete($table, $where, $params = []) {
$sql = "DELETE FROM {$table} WHERE {$where}";
return $this->query($sql, $params)->rowCount();
}
/**
* Begin transaction
*/
public function beginTransaction() {
return $this->connection->beginTransaction();
}
/**
* Commit transaction
*/
public function commit() {
return $this->connection->commit();
}
/**
* Rollback transaction
*/
public function rollback() {
return $this->connection->rollBack();
}
/**
* Get last insert ID
*/
public function lastInsertId() {
return $this->connection->lastInsertId();
}
/**
* Log database errors
*/
private function logError($message, $sql, $params) {
$logFile = __DIR__ . '/../logs/database.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] Error: {$message}\n";
$logMessage .= "SQL: {$sql}\n";
$logMessage .= "Params: " . json_encode($params) . "\n";
$logMessage .= "------------------------\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
* Prevent cloning of the instance
*/
private function __clone() {}
/**
* Prevent unserializing of the instance
*/
public function __wakeup() {}
}
?>
Configuration File
File: includes/config.php
<?php
/**
* Configuration File
* Loads environment variables and sets up constants
*/
// Start session if not started
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Load environment variables from .env file
function loadEnv($path) {
if (!file_exists($path)) {
return false;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!array_key_exists($name, $_ENV)) {
$_ENV[$name] = $value;
putenv(sprintf('%s=%s', $name, $value));
}
}
return true;
}
// Load environment variables
loadEnv(__DIR__ . '/../.env');
// Database Configuration
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_NAME', getenv('DB_NAME') ?: 'notes_app');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASS', getenv('DB_PASS') ?: '');
// Application Configuration
define('SITE_NAME', getenv('SITE_NAME') ?: 'Notes App');
define('SITE_URL', getenv('SITE_URL') ?: 'http://localhost/notes-app');
define('SITE_EMAIL', getenv('SITE_EMAIL') ?: '[email protected]');
define('APP_VERSION', getenv('APP_VERSION') ?: '1.0.0');
define('DEBUG_MODE', getenv('DEBUG_MODE') === 'true');
// Security Configuration
define('SESSION_TIMEOUT', getenv('SESSION_TIMEOUT') ?: 3600); // 1 hour
define('BCRYPT_ROUNDS', 12);
define('CSRF_TOKEN_NAME', 'csrf_token');
define('PEPPER', getenv('PEPPER') ?: 'default-pepper-string-change-this');
// Upload Configuration
define('UPLOAD_DIR', __DIR__ . '/../uploads/');
define('MAX_FILE_SIZE', getenv('MAX_FILE_SIZE') ?: 5 * 1024 * 1024); // 5MB
define('ALLOWED_EXTENSIONS', explode(',', getenv('ALLOWED_FILE_TYPES') ?: 'jpg,jpeg,png,gif,pdf,doc,docx,txt,md'));
// Storage Limits (in bytes)
define('BASIC_STORAGE_LIMIT', getenv('BASIC_STORAGE_LIMIT') ?: 100 * 1024 * 1024); // 100 MB
define('PREMIUM_STORAGE_LIMIT', getenv('PREMIUM_STORAGE_LIMIT') ?: 1024 * 1024 * 1024); // 1 GB
// Pagination
define('ITEMS_PER_PAGE', getenv('ITEMS_PER_PAGE') ?: 20);
// Date/Time Configuration
date_default_timezone_set(getenv('TIMEZONE') ?: 'America/New_York');
define('DATE_FORMAT', 'Y-m-d');
define('TIME_FORMAT', 'H:i');
define('DATETIME_FORMAT', 'Y-m-d H:i:s');
// Feature Flags
define('ENABLE_RICH_EDITOR', getenv('ENABLE_RICH_EDITOR') === 'true');
define('ENABLE_MARKDOWN', getenv('ENABLE_MARKDOWN') === 'true');
define('ENABLE_ATTACHMENTS', getenv('ENABLE_ATTACHMENTS') === 'true');
define('ENABLE_SHARING', getenv('ENABLE_SHARING') === 'true');
define('ENABLE_REMINDERS', getenv('ENABLE_REMINDERS') === 'true');
// Mail Configuration
define('MAIL_DRIVER', getenv('MAIL_DRIVER') ?: 'smtp');
define('MAIL_HOST', getenv('MAIL_HOST') ?: 'smtp.gmail.com');
define('MAIL_PORT', getenv('MAIL_PORT') ?: 587);
define('MAIL_USERNAME', getenv('MAIL_USERNAME') ?: '');
define('MAIL_PASSWORD', getenv('MAIL_PASSWORD') ?: '');
define('MAIL_ENCRYPTION', getenv('MAIL_ENCRYPTION') ?: 'tls');
define('MAIL_FROM_ADDRESS', getenv('MAIL_FROM_ADDRESS') ?: '[email protected]');
define('MAIL_FROM_NAME', getenv('MAIL_FROM_NAME') ?: SITE_NAME);
// Error Reporting
if (DEBUG_MODE) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
error_reporting(0);
ini_set('display_errors', 0);
}
// Include required files
require_once __DIR__ . '/Database.php';
require_once __DIR__ . '/functions.php';
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/Note.php';
require_once __DIR__ . '/Category.php';
require_once __DIR__ . '/Tag.php';
require_once __DIR__ . '/User.php';
require_once __DIR__ . '/Attachment.php';
// Initialize database connection
$db = Database::getInstance();
// Load system settings
$settings = $db->getRows("SELECT setting_key, setting_value FROM system_settings");
foreach ($settings as $setting) {
define(strtoupper($setting['setting_key']), $setting['setting_value']);
}
// Set timezone for MySQL
$db->query("SET time_zone = ?", [date('P')]);
// Initialize session with timeout
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > SESSION_TIMEOUT)) {
session_unset();
session_destroy();
}
$_SESSION['last_activity'] = time();
?>
Helper Functions
File: includes/functions.php
<?php
/**
* Helper Functions
* Common utility functions used throughout the application
*/
/**
* Sanitize input data
*/
function sanitize($input) {
if (is_array($input)) {
return array_map('sanitize', $input);
}
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
/**
* Generate CSRF token
*/
function generateCSRFToken() {
if (!isset($_SESSION[CSRF_TOKEN_NAME])) {
$_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
}
return $_SESSION[CSRF_TOKEN_NAME];
}
/**
* Verify CSRF token
*/
function verifyCSRFToken($token) {
if (!isset($_SESSION[CSRF_TOKEN_NAME]) || $token !== $_SESSION[CSRF_TOKEN_NAME]) {
return false;
}
return true;
}
/**
* Redirect to URL
*/
function redirect($url) {
header("Location: " . SITE_URL . $url);
exit();
}
/**
* Generate UUID v4
*/
function generateUUID() {
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
/**
* Create slug from string
*/
function createSlug($string) {
$string = preg_replace('/[^a-zA-Z0-9\s]/', '', $string);
$string = strtolower(trim($string));
$string = preg_replace('/\s+/', '-', $string);
return $string;
}
/**
* Generate unique slug for note
*/
function generateUniqueSlug($userId, $title, $db, $noteId = null) {
$slug = createSlug($title);
$originalSlug = $slug;
$counter = 1;
$query = "SELECT id FROM notes WHERE user_id = :user_id AND slug = :slug";
$params = ['user_id' => $userId, 'slug' => $slug];
if ($noteId) {
$query .= " AND id != :note_id";
$params['note_id'] = $noteId;
}
while ($db->getRow($query, $params)) {
$slug = $originalSlug . '-' . $counter;
$params['slug'] = $slug;
$counter++;
}
return $slug;
}
/**
* Get user IP address
*/
function getUserIP() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
return $_SERVER['REMOTE_ADDR'];
}
}
/**
* Format file size
*/
function formatFileSize($bytes, $decimals = 2) {
$size = ['B', 'KB', 'MB', 'GB', 'TB'];
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . $size[$factor];
}
/**
* Calculate reading time
*/
function calculateReadingTime($content, $wordsPerMinute = 200) {
$wordCount = str_word_count(strip_tags($content));
return ceil($wordCount / $wordsPerMinute);
}
/**
* Get excerpt from content
*/
function getExcerpt($content, $length = 200) {
$text = strip_tags($content);
if (strlen($text) <= $length) {
return $text;
}
return substr($text, 0, $length) . '...';
}
/**
* Format date
*/
function formatDate($date, $format = null) {
if ($format === null) {
$format = DATE_FORMAT;
}
if ($date instanceof DateTime) {
return $date->format($format);
}
return date($format, strtotime($date));
}
/**
* Format time
*/
function formatTime($time, $format = null) {
if ($format === null) {
$format = TIME_FORMAT;
}
return date($format, strtotime($time));
}
/**
* Get time ago string
*/
function timeAgo($datetime) {
$time = strtotime($datetime);
$now = time();
$diff = $now - $time;
if ($diff < 60) {
return $diff . ' seconds ago';
} elseif ($diff < 3600) {
return floor($diff / 60) . ' minutes ago';
} elseif ($diff < 86400) {
return floor($diff / 3600) . ' hours ago';
} elseif ($diff < 2592000) {
return floor($diff / 86400) . ' days ago';
} elseif ($diff < 31536000) {
return floor($diff / 2592000) . ' months ago';
} else {
return floor($diff / 31536000) . ' years ago';
}
}
/**
* Check user storage limit
*/
function checkStorageLimit($userId, $fileSize) {
$db = Database::getInstance();
$user = $db->getRow("SELECT storage_used, storage_limit FROM users WHERE id = ?", [$userId]);
if ($user['storage_used'] + $fileSize > $user['storage_limit']) {
return false;
}
return true;
}
/**
* Update user storage usage
*/
function updateStorageUsage($userId, $fileSize, $operation = 'add') {
$db = Database::getInstance();
if ($operation === 'add') {
$db->query("UPDATE users SET storage_used = storage_used + ? WHERE id = ?", [$fileSize, $userId]);
} else {
$db->query("UPDATE users SET storage_used = GREATEST(0, storage_used - ?) WHERE id = ?", [$fileSize, $userId]);
}
}
/**
* Upload file
*/
function uploadFile($file, $targetDir, $userId, $allowedTypes = null) {
if ($allowedTypes === null) {
$allowedTypes = ALLOWED_EXTENSIONS;
}
// Check for errors
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => 'Upload failed with error code: ' . $file['error']];
}
// Check file size
if ($file['size'] > MAX_FILE_SIZE) {
return ['success' => false, 'error' => 'File size exceeds limit'];
}
// Check storage limit
if (!checkStorageLimit($userId, $file['size'])) {
return ['success' => false, 'error' => 'Storage limit exceeded'];
}
// Check file type
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedTypes)) {
return ['success' => false, 'error' => 'File type not allowed'];
}
// Generate unique filename
$filename = uniqid() . '_' . time() . '.' . $extension;
$targetPath = $targetDir . '/' . $filename;
// Create directory if not exists
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
// Upload file
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
// Update storage usage
updateStorageUsage($userId, $file['size'], 'add');
return [
'success' => true,
'filename' => $filename,
'original_name' => $file['name'],
'path' => $targetPath,
'size' => $file['size'],
'type' => $file['type'],
'extension' => $extension,
'url' => SITE_URL . '/uploads/' . basename($targetDir) . '/' . $filename
];
}
return ['success' => false, 'error' => 'Failed to move uploaded file'];
}
/**
* Delete file
*/
function deleteFile($path, $userId, $fileSize = null) {
if (file_exists($path)) {
$size = $fileSize ?: filesize($path);
if (unlink($path)) {
updateStorageUsage($userId, $size, 'remove');
return true;
}
}
return false;
}
/**
* Generate random string
*/
function generateRandomString($length = 32) {
return bin2hex(random_bytes($length / 2));
}
/**
* Validate email
*/
function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
* Validate URL
*/
function validateURL($url) {
return filter_var($url, FILTER_VALIDATE_URL);
}
/**
* Send email notification
*/
function sendEmail($to, $subject, $template, $data = []) {
// Load email template
$templateFile = __DIR__ . "/templates/email/{$template}.php";
if (!file_exists($templateFile)) {
logError("Email template not found: {$template}");
return false;
}
// Extract data for template
extract($data);
ob_start();
include $templateFile;
$message = ob_get_clean();
// Headers
$headers = [
'MIME-Version: 1.0',
'Content-type: text/html; charset=utf-8',
'From: ' . MAIL_FROM_NAME . ' <' . MAIL_FROM_ADDRESS . '>',
'Reply-To: ' . MAIL_FROM_ADDRESS,
'X-Mailer: PHP/' . phpversion()
];
return mail($to, $subject, $message, implode("\r\n", $headers));
}
/**
* Log error
*/
function logError($message, $context = []) {
$logFile = __DIR__ . '/../logs/error.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$contextStr = !empty($context) ? ' ' . json_encode($context) : '';
$logMessage = "[{$timestamp}] {$message}{$contextStr}\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
* Log audit trail
*/
function logAudit($userId, $action, $description = '', $data = []) {
$db = Database::getInstance();
$db->insert('audit_logs', [
'user_id' => $userId,
'action' => $action,
'description' => $description,
'ip_address' => getUserIP(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'data' => json_encode($data)
]);
}
/**
* Generate pagination
*/
function paginate($currentPage, $totalPages, $url) {
if ($totalPages <= 1) {
return '';
}
$html = '<nav aria-label="Page navigation"><ul class="pagination justify-content-center">';
// Previous button
if ($currentPage > 1) {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '&page=' . ($currentPage - 1) . '">Previous</a></li>';
} else {
$html .= '<li class="page-item disabled"><span class="page-link">Previous</span></li>';
}
// Page numbers
for ($i = 1; $i <= $totalPages; $i++) {
if ($i == $currentPage) {
$html .= '<li class="page-item active"><span class="page-link">' . $i . '</span></li>';
} else {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '&page=' . $i . '">' . $i . '</a></li>';
}
}
// Next button
if ($currentPage < $totalPages) {
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '&page=' . ($currentPage + 1) . '">Next</a></li>';
} else {
$html .= '<li class="page-item disabled"><span class="page-link">Next</span></li>';
}
$html .= '</ul></nav>';
return $html;
}
/**
* Get user role badge
*/
function getUserRoleBadge($role) {
$badges = [
'admin' => 'bg-danger',
'premium' => 'bg-warning',
'basic' => 'bg-secondary'
];
$labels = [
'admin' => 'Admin',
'premium' => 'Premium',
'basic' => 'Basic'
];
$class = $badges[$role] ?? 'bg-secondary';
$label = $labels[$role] ?? ucfirst($role);
return '<span class="badge ' . $class . '">' . $label . '</span>';
}
/**
* Get note status badge
*/
function getNoteStatusBadge($note) {
if ($note['is_trashed']) {
return '<span class="badge bg-danger">Trashed</span>';
}
if ($note['is_archived']) {
return '<span class="badge bg-warning">Archived</span>';
}
if ($note['is_favorite']) {
return '<span class="badge bg-success">Favorite</span>';
}
if ($note['is_pinned']) {
return '<span class="badge bg-info">Pinned</span>';
}
return '<span class="badge bg-secondary">Active</span>';
}
/**
* Convert Markdown to HTML
*/
function markdownToHtml($markdown) {
// This is a simplified version. In production, use Parsedown library
$html = $markdown;
// Headers
$html = preg_replace('/^### (.*?)$/m', '<h3>$1</h3>', $html);
$html = preg_replace('/^## (.*?)$/m', '<h2>$1</h2>', $html);
$html = preg_replace('/^# (.*?)$/m', '<h1>$1</h1>', $html);
// Bold and italic
$html = preg_replace('/\*\*\*(.*?)\*\*\*/', '<strong><em>$1</em></strong>', $html);
$html = preg_replace('/\*\*(.*?)\*\*/', '<strong>$1</strong>', $html);
$html = preg_replace('/\*(.*?)\*/', '<em>$1</em>', $html);
// Lists
$html = preg_replace('/^\- (.*?)$/m', '<li>$1</li>', $html);
$html = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $html);
return $html;
}
/**
* Send push notification (placeholder)
*/
function sendPushNotification($userId, $title, $message) {
// Implement push notification logic here
// This could use Firebase Cloud Messaging or similar
return true;
}
?>
Authentication Class
File: includes/auth.php
<?php
/**
* Authentication Class
* Handles user authentication, registration, and session management
*/
class Auth {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Register new user
*/
public function register($data) {
try {
// Check if username or email already exists
$existing = $this->db->getRow(
"SELECT id FROM users WHERE username = ? OR email = ?",
[$data['username'], $data['email']]
);
if ($existing) {
return ['success' => false, 'error' => 'Username or email already exists'];
}
// Hash password with pepper
$pepperedPassword = $data['password'] . PEPPER;
$hashedPassword = password_hash($pepperedPassword, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
// Generate verification token
$verificationToken = generateRandomString();
// Determine storage limit based on role
$storageLimit = ($data['role'] ?? 'basic') === 'premium' ? PREMIUM_STORAGE_LIMIT : BASIC_STORAGE_LIMIT;
// Insert user
$userId = $this->db->insert('users', [
'username' => $data['username'],
'email' => $data['email'],
'password' => $hashedPassword,
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'display_name' => $data['display_name'] ?? $data['first_name'] . ' ' . $data['last_name'],
'role' => $data['role'] ?? 'basic',
'storage_limit' => $storageLimit,
'verification_token' => $verificationToken,
'last_ip' => getUserIP()
]);
if ($userId) {
// Create default categories for user
$this->createDefaultCategories($userId);
// Send verification email
if (REQUIRE_EMAIL_VERIFICATION) {
$this->sendVerificationEmail($data['email'], $verificationToken);
} else {
// Auto-verify
$this->db->update('users', ['email_verified' => true], 'id = :id', ['id' => $userId]);
}
logAudit($userId, 'register', 'User registered');
return [
'success' => true,
'user_id' => $userId,
'message' => REQUIRE_EMAIL_VERIFICATION ?
'Registration successful. Please check your email to verify your account.' :
'Registration successful. You can now login.'
];
}
return ['success' => false, 'error' => 'Registration failed'];
} catch (Exception $e) {
logError('Registration error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Registration failed: ' . $e->getMessage()];
}
}
/**
* Create default categories for new user
*/
private function createDefaultCategories($userId) {
$categories = [
['name' => 'Personal', 'color' => '#28a745', 'icon' => 'fa-user'],
['name' => 'Work', 'color' => '#007bff', 'icon' => 'fa-briefcase'],
['name' => 'Ideas', 'color' => '#ffc107', 'icon' => 'fa-lightbulb'],
['name' => 'Journal', 'color' => '#6f42c1', 'icon' => 'fa-book'],
['name' => 'Tasks', 'color' => '#dc3545', 'icon' => 'fa-tasks']
];
foreach ($categories as $category) {
$category['user_id'] = $userId;
$category['slug'] = createSlug($category['name']);
$category['is_system'] = true;
$this->db->insert('categories', $category);
}
}
/**
* Login user
*/
public function login($username, $password, $remember = false) {
try {
// Get user
$user = $this->db->getRow(
"SELECT * FROM users WHERE (username = ? OR email = ?) AND status = 'active'",
[$username, $username]
);
if (!$user) {
return ['success' => false, 'error' => 'Invalid username or password'];
}
// Check if email verified
if (REQUIRE_EMAIL_VERIFICATION && !$user['email_verified']) {
return ['success' => false, 'error' => 'Please verify your email before logging in'];
}
// Verify password with pepper
$pepperedPassword = $password . PEPPER;
if (!password_verify($pepperedPassword, $user['password'])) {
return ['success' => false, 'error' => 'Invalid username or password'];
}
// Check if password needs rehash
if (password_needs_rehash($user['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS])) {
$newHash = password_hash($pepperedPassword, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update('users', ['password' => $newHash], 'id = :id', ['id' => $user['id']]);
}
// Set session
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['user_name'] = $user['display_name'] ?? $user['first_name'] . ' ' . $user['last_name'];
$_SESSION['logged_in'] = true;
$_SESSION['login_time'] = time();
// Update last login
$this->db->update(
'users',
[
'last_login' => date('Y-m-d H:i:s'),
'last_ip' => getUserIP()
],
'id = :id',
['id' => $user['id']]
);
// Set remember me cookie
if ($remember) {
$this->setRememberMe($user['id']);
}
logAudit($user['id'], 'login', 'User logged in');
return ['success' => true, 'user' => $user];
} catch (Exception $e) {
logError('Login error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Login failed'];
}
}
/**
* Set remember me cookie
*/
private function setRememberMe($userId) {
$token = generateRandomString(64);
$expires = time() + (86400 * 30); // 30 days
// Store token in database (you would need a remember_tokens table)
// For now, just set cookie
setcookie('remember_token', $token, $expires, '/', '', false, true);
}
/**
* Logout user
*/
public function logout() {
if (isset($_SESSION['user_id'])) {
logAudit($_SESSION['user_id'], 'logout', 'User logged out');
}
// Clear session
$_SESSION = array();
// Clear session cookie
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
// Clear remember me cookie
setcookie('remember_token', '', time() - 3600, '/');
// Destroy session
session_destroy();
}
/**
* Check if user is logged in
*/
public function isLoggedIn() {
return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
}
/**
* Get current user
*/
public function getCurrentUser() {
if (!$this->isLoggedIn()) {
return null;
}
return $this->db->getRow(
"SELECT * FROM users WHERE id = ?",
[$_SESSION['user_id']]
);
}
/**
* Check if user has role
*/
public function hasRole($role) {
if (!$this->isLoggedIn()) {
return false;
}
if (is_array($role)) {
return in_array($_SESSION['user_role'], $role);
}
return $_SESSION['user_role'] === $role;
}
/**
* Check if user is premium
*/
public function isPremium() {
if (!$this->isLoggedIn()) {
return false;
}
$user = $this->getCurrentUser();
if ($user['role'] === 'premium' || $user['role'] === 'admin') {
// Check if premium hasn't expired
if ($user['premium_until'] && strtotime($user['premium_until']) < time()) {
// Downgrade to basic
$this->downgradeUser($user['id']);
return false;
}
return true;
}
return false;
}
/**
* Downgrade user from premium to basic
*/
private function downgradeUser($userId) {
$this->db->update(
'users',
['role' => 'basic', 'premium_until' => null],
'id = :id',
['id' => $userId]
);
// Update storage limit
$this->db->update(
'users',
['storage_limit' => BASIC_STORAGE_LIMIT],
'id = :id',
['id' => $userId]
);
}
/**
* Require login
*/
public function requireLogin() {
if (!$this->isLoggedIn()) {
$_SESSION['error'] = 'Please login to access this page';
redirect('/login.php');
}
}
/**
* Require role
*/
public function requireRole($role) {
$this->requireLogin();
if (!$this->hasRole($role)) {
$_SESSION['error'] = 'You do not have permission to access this page';
// Redirect based on role
if ($this->hasRole('admin')) {
redirect('/admin/dashboard.php');
} elseif ($this->hasRole('premium')) {
redirect('/user/dashboard.php');
} else {
redirect('/user/dashboard.php');
}
}
}
/**
* Require premium
*/
public function requirePremium() {
$this->requireLogin();
if (!$this->isPremium()) {
$_SESSION['error'] = 'This feature requires a premium subscription';
redirect('/user/upgrade.php');
}
}
/**
* Verify email
*/
public function verifyEmail($token) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE verification_token = ?",
[$token]
);
if ($user) {
$this->db->update(
'users',
['email_verified' => true, 'verification_token' => null],
'id = :id',
['id' => $user['id']]
);
logAudit($user['id'], 'verify_email', 'Email verified');
return true;
}
return false;
}
/**
* Send verification email
*/
private function sendVerificationEmail($email, $token) {
$subject = "Verify your email - " . SITE_NAME;
$data = [
'verification_link' => SITE_URL . "/verify.php?token=" . $token
];
return sendEmail($email, $subject, 'verification', $data);
}
/**
* Forgot password
*/
public function forgotPassword($email) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE email = ?",
[$email]
);
if ($user) {
$token = generateRandomString();
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
$this->db->update(
'users',
['reset_token' => $token, 'reset_expires' => $expires],
'id = :id',
['id' => $user['id']]
);
// Send reset email
$subject = "Password Reset - " . SITE_NAME;
$data = [
'reset_link' => SITE_URL . "/reset_password.php?token=" . $token
];
return sendEmail($email, $subject, 'password_reset', $data);
}
return false;
}
/**
* Reset password
*/
public function resetPassword($token, $password) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE reset_token = ? AND reset_expires > NOW()",
[$token]
);
if ($user) {
$pepperedPassword = $password . PEPPER;
$hashedPassword = password_hash($pepperedPassword, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update(
'users',
['password' => $hashedPassword, 'reset_token' => null, 'reset_expires' => null],
'id = :id',
['id' => $user['id']]
);
logAudit($user['id'], 'reset_password', 'Password reset');
return true;
}
return false;
}
/**
* Update user profile
*/
public function updateProfile($userId, $data) {
try {
$updateData = [
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'display_name' => $data['display_name'],
'bio' => $data['bio'] ?? null,
'theme' => $data['theme'] ?? 'light',
'default_view' => $data['default_view'] ?? 'grid',
'items_per_page' => $data['items_per_page'] ?? 20
];
// Handle profile picture upload
if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'avatars/';
$result = uploadFile($_FILES['profile_picture'], $uploadDir, $userId);
if ($result['success']) {
// Delete old picture
$user = $this->getCurrentUser();
if ($user['profile_picture'] !== 'default.png') {
deleteFile(UPLOAD_DIR . 'avatars/' . $user['profile_picture'], $userId);
}
$updateData['profile_picture'] = $result['filename'];
}
}
$this->db->update('users', $updateData, 'id = :id', ['id' => $userId]);
logAudit($userId, 'update_profile', 'Profile updated');
return ['success' => true, 'message' => 'Profile updated successfully'];
} catch (Exception $e) {
logError('Profile update error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to update profile'];
}
}
/**
* Change password
*/
public function changePassword($userId, $currentPassword, $newPassword) {
$user = $this->db->getRow("SELECT password FROM users WHERE id = ?", [$userId]);
$pepperedCurrent = $currentPassword . PEPPER;
if (!password_verify($pepperedCurrent, $user['password'])) {
return ['success' => false, 'error' => 'Current password is incorrect'];
}
$pepperedNew = $newPassword . PEPPER;
$hashedPassword = password_hash($pepperedNew, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update('users', ['password' => $hashedPassword], 'id = :id', ['id' => $userId]);
logAudit($userId, 'change_password', 'Password changed');
return ['success' => true, 'message' => 'Password changed successfully'];
}
/**
* Get user by ID
*/
public function getUserById($userId) {
return $this->db->getRow("SELECT * FROM users WHERE id = ?", [$userId]);
}
/**
* Get user storage info
*/
public function getStorageInfo($userId) {
$user = $this->getUserById($userId);
$used = $user['storage_used'];
$limit = $user['storage_limit'];
$percentage = $limit > 0 ? round(($used / $limit) * 100, 2) : 0;
return [
'used' => $used,
'limit' => $limit,
'percentage' => $percentage,
'used_formatted' => formatFileSize($used),
'limit_formatted' => formatFileSize($limit),
'is_near_limit' => $percentage > 80,
'is_full' => $percentage >= 100
];
}
}
// Initialize Auth
$auth = new Auth();
?>
Note Class
File: includes/Note.php
<?php
/**
* Note Class
* Handles all note-related operations
*/
class Note {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Create a new note
*/
public function create($userId, $data) {
try {
$this->db->beginTransaction();
// Generate UUID and slug
$uuid = generateUUID();
$slug = generateUniqueSlug($userId, $data['title'], $this->db);
// Calculate word count and reading time
$wordCount = str_word_count(strip_tags($data['content'] ?? ''));
$readingTime = ceil($wordCount / 200);
// Prepare note data
$noteData = [
'user_id' => $userId,
'category_id' => $data['category_id'] ?? null,
'uuid' => $uuid,
'title' => $data['title'],
'slug' => $slug,
'content' => $data['content'] ?? '',
'content_plain' => strip_tags($data['content'] ?? ''),
'excerpt' => getExcerpt($data['content'] ?? ''),
'editor_type' => $data['editor_type'] ?? 'rich',
'color' => $data['color'] ?? null,
'icon' => $data['icon'] ?? null,
'word_count' => $wordCount,
'character_count' => strlen($data['content'] ?? ''),
'reading_time' => $readingTime
];
// Insert note
$noteId = $this->db->insert('notes', $noteData);
// Handle tags
if (!empty($data['tags'])) {
$this->addTags($noteId, $userId, $data['tags']);
}
// Update category notes count
if (!empty($data['category_id'])) {
$this->db->query(
"UPDATE categories SET notes_count = notes_count + 1 WHERE id = ?",
[$data['category_id']]
);
}
// Update user notes count
$this->db->query(
"UPDATE users SET notes_count = notes_count + 1 WHERE id = ?",
[$userId]
);
$this->db->commit();
logAudit($userId, 'create_note', 'Created new note', ['note_id' => $noteId]);
return [
'success' => true,
'note_id' => $noteId,
'uuid' => $uuid,
'slug' => $slug,
'message' => 'Note created successfully'
];
} catch (Exception $e) {
$this->db->rollback();
logError('Create note error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Failed to create note: ' . $e->getMessage()];
}
}
/**
* Update note
*/
public function update($noteId, $userId, $data) {
try {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return ['success' => false, 'error' => 'Note not found'];
}
$this->db->beginTransaction();
// Generate new slug if title changed
$slug = $note['slug'];
if ($data['title'] !== $note['title']) {
$slug = generateUniqueSlug($userId, $data['title'], $this->db, $noteId);
}
// Calculate word count and reading time
$wordCount = str_word_count(strip_tags($data['content'] ?? ''));
$readingTime = ceil($wordCount / 200);
// Prepare update data
$updateData = [
'category_id' => $data['category_id'] ?? $note['category_id'],
'title' => $data['title'],
'slug' => $slug,
'content' => $data['content'] ?? $note['content'],
'content_plain' => strip_tags($data['content'] ?? $note['content']),
'excerpt' => getExcerpt($data['content'] ?? $note['content']),
'editor_type' => $data['editor_type'] ?? $note['editor_type'],
'color' => $data['color'] ?? $note['color'],
'icon' => $data['icon'] ?? $note['icon'],
'word_count' => $wordCount,
'character_count' => strlen($data['content'] ?? ''),
'reading_time' => $readingTime,
'version' => $note['version'] + 1
];
// Save version for premium users
$user = (new Auth())->getCurrentUser();
if ($user['role'] === 'premium' || $user['role'] === 'admin') {
$this->saveVersion($noteId, $note['title'], $note['content'], $userId);
}
// Update note
$this->db->update('notes', $updateData, 'id = :id', ['id' => $noteId]);
// Handle category change
if ($note['category_id'] != $updateData['category_id']) {
// Decrement old category
if ($note['category_id']) {
$this->db->query(
"UPDATE categories SET notes_count = GREATEST(0, notes_count - 1) WHERE id = ?",
[$note['category_id']]
);
}
// Increment new category
if ($updateData['category_id']) {
$this->db->query(
"UPDATE categories SET notes_count = notes_count + 1 WHERE id = ?",
[$updateData['category_id']]
);
}
}
// Handle tags
if (isset($data['tags'])) {
$this->updateTags($noteId, $userId, $data['tags']);
}
$this->db->commit();
logAudit($userId, 'update_note', 'Updated note', ['note_id' => $noteId]);
return ['success' => true, 'message' => 'Note updated successfully'];
} catch (Exception $e) {
$this->db->rollback();
logError('Update note error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to update note'];
}
}
/**
* Save note version
*/
private function saveVersion($noteId, $title, $content, $userId) {
$version = $this->db->getValue(
"SELECT COUNT(*) + 1 FROM note_versions WHERE note_id = ?",
[$noteId]
);
$this->db->insert('note_versions', [
'note_id' => $noteId,
'version' => $version,
'title' => $title,
'content' => $content,
'word_count' => str_word_count(strip_tags($content)),
'character_count' => strlen($content),
'created_by' => $userId
]);
}
/**
* Add tags to note
*/
private function addTags($noteId, $userId, $tags) {
$tagIds = [];
foreach ($tags as $tagName) {
$tagName = trim($tagName);
if (empty($tagName)) continue;
$slug = createSlug($tagName);
// Check if tag exists
$tag = $this->db->getRow(
"SELECT id FROM tags WHERE user_id = ? AND slug = ?",
[$userId, $slug]
);
if ($tag) {
$tagId = $tag['id'];
// Increment tag count
$this->db->query(
"UPDATE tags SET notes_count = notes_count + 1 WHERE id = ?",
[$tagId]
);
} else {
// Create new tag
$tagId = $this->db->insert('tags', [
'user_id' => $userId,
'name' => $tagName,
'slug' => $slug,
'notes_count' => 1
]);
}
$tagIds[] = $tagId;
}
// Associate tags with note
foreach ($tagIds as $tagId) {
$this->db->insert('note_tags', [
'note_id' => $noteId,
'tag_id' => $tagId
]);
}
}
/**
* Update note tags
*/
private function updateTags($noteId, $userId, $newTags) {
// Get current tags
$currentTags = $this->db->getRows(
"SELECT t.id, t.name FROM note_tags nt
JOIN tags t ON nt.tag_id = t.id
WHERE nt.note_id = ?",
[$noteId]
);
$currentTagIds = array_column($currentTags, 'id');
$currentTagNames = array_column($currentTags, 'name');
// Tags to remove
$tagsToRemove = array_diff($currentTagNames, $newTags);
foreach ($tagsToRemove as $tagName) {
$tag = $this->db->getRow(
"SELECT id FROM tags WHERE user_id = ? AND name = ?",
[$userId, $tagName]
);
if ($tag) {
// Remove association
$this->db->delete(
'note_tags',
'note_id = :note_id AND tag_id = :tag_id',
['note_id' => $noteId, 'tag_id' => $tag['id']]
);
// Decrement tag count
$this->db->query(
"UPDATE tags SET notes_count = GREATEST(0, notes_count - 1) WHERE id = ?",
[$tag['id']]
);
}
}
// Tags to add
$tagsToAdd = array_diff($newTags, $currentTagNames);
foreach ($tagsToAdd as $tagName) {
$tagName = trim($tagName);
if (empty($tagName)) continue;
$slug = createSlug($tagName);
$tag = $this->db->getRow(
"SELECT id FROM tags WHERE user_id = ? AND slug = ?",
[$userId, $slug]
);
if ($tag) {
$tagId = $tag['id'];
$this->db->query(
"UPDATE tags SET notes_count = notes_count + 1 WHERE id = ?",
[$tagId]
);
} else {
$tagId = $this->db->insert('tags', [
'user_id' => $userId,
'name' => $tagName,
'slug' => $slug,
'notes_count' => 1
]);
}
$this->db->insert('note_tags', [
'note_id' => $noteId,
'tag_id' => $tagId
]);
}
}
/**
* Delete note (soft delete)
*/
public function delete($noteId, $userId) {
try {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return ['success' => false, 'error' => 'Note not found'];
}
// Soft delete
$this->db->update(
'notes',
[
'is_trashed' => true,
'deleted_at' => date('Y-m-d H:i:s')
],
'id = :id',
['id' => $noteId]
);
logAudit($userId, 'delete_note', 'Moved note to trash', ['note_id' => $noteId]);
return ['success' => true, 'message' => 'Note moved to trash'];
} catch (Exception $e) {
logError('Delete note error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to delete note'];
}
}
/**
* Permanently delete note
*/
public function permanentlyDelete($noteId, $userId) {
try {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return ['success' => false, 'error' => 'Note not found'];
}
$this->db->beginTransaction();
// Delete attachments
$attachments = $this->db->getRows(
"SELECT * FROM attachments WHERE note_id = ?",
[$noteId]
);
foreach ($attachments as $attachment) {
$filePath = UPLOAD_DIR . 'attachments/' . $attachment['file_path'];
deleteFile($filePath, $userId, $attachment['file_size']);
$this->db->delete('attachments', 'id = :id', ['id' => $attachment['id']]);
}
// Update category notes count
if ($note['category_id']) {
$this->db->query(
"UPDATE categories SET notes_count = GREATEST(0, notes_count - 1) WHERE id = ?",
[$note['category_id']]
);
}
// Update user notes count
$this->db->query(
"UPDATE users SET notes_count = GREATEST(0, notes_count - 1) WHERE id = ?",
[$userId]
);
// Delete note
$this->db->delete('notes', 'id = :id', ['id' => $noteId]);
$this->db->commit();
logAudit($userId, 'permanent_delete', 'Permanently deleted note', ['note_id' => $noteId]);
return ['success' => true, 'message' => 'Note permanently deleted'];
} catch (Exception $e) {
$this->db->rollback();
logError('Permanent delete error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to delete note'];
}
}
/**
* Restore note from trash
*/
public function restore($noteId, $userId) {
try {
$this->db->update(
'notes',
[
'is_trashed' => false,
'deleted_at' => null
],
'id = :id',
['id' => $noteId]
);
logAudit($userId, 'restore_note', 'Restored note from trash', ['note_id' => $noteId]);
return ['success' => true, 'message' => 'Note restored successfully'];
} catch (Exception $e) {
logError('Restore note error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to restore note'];
}
}
/**
* Toggle favorite status
*/
public function toggleFavorite($noteId, $userId) {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return ['success' => false, 'error' => 'Note not found'];
}
$newStatus = !$note['is_favorite'];
$this->db->update(
'notes',
['is_favorite' => $newStatus],
'id = :id',
['id' => $noteId]
);
$message = $newStatus ? 'Note added to favorites' : 'Note removed from favorites';
return ['success' => true, 'is_favorite' => $newStatus, 'message' => $message];
}
/**
* Toggle archive status
*/
public function toggleArchive($noteId, $userId) {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return ['success' => false, 'error' => 'Note not found'];
}
$newStatus = !$note['is_archived'];
$this->db->update(
'notes',
['is_archived' => $newStatus],
'id = :id',
['id' => $noteId]
);
$message = $newStatus ? 'Note archived' : 'Note unarchived';
return ['success' => true, 'is_archived' => $newStatus, 'message' => $message];
}
/**
* Toggle pin status
*/
public function togglePin($noteId, $userId) {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return ['success' => false, 'error' => 'Note not found'];
}
$newStatus = !$note['is_pinned'];
$this->db->update(
'notes',
['is_pinned' => $newStatus],
'id = :id',
['id' => $noteId]
);
$message = $newStatus ? 'Note pinned' : 'Note unpinned';
return ['success' => true, 'is_pinned' => $newStatus, 'message' => $message];
}
/**
* Get note by ID
*/
public function getNoteById($noteId, $userId = null) {
$sql = "SELECT n.*, c.name as category_name, c.color as category_color
FROM notes n
LEFT JOIN categories c ON n.category_id = c.id
WHERE n.id = ?";
$params = [$noteId];
if ($userId) {
$sql .= " AND n.user_id = ?";
$params[] = $userId;
}
return $this->db->getRow($sql, $params);
}
/**
* Get note by UUID
*/
public function getNoteByUuid($uuid, $userId = null) {
$sql = "SELECT n.*, c.name as category_name, c.color as category_color
FROM notes n
LEFT JOIN categories c ON n.category_id = c.id
WHERE n.uuid = ?";
$params = [$uuid];
if ($userId) {
$sql .= " AND n.user_id = ?";
$params[] = $userId;
}
return $this->db->getRow($sql, $params);
}
/**
* Get note by slug
*/
public function getNoteBySlug($slug, $userId) {
return $this->db->getRow(
"SELECT n.*, c.name as category_name, c.color as category_color
FROM notes n
LEFT JOIN categories c ON n.category_id = c.id
WHERE n.slug = ? AND n.user_id = ?",
[$slug, $userId]
);
}
/**
* Get user notes with filters
*/
public function getUserNotes($userId, $filters = []) {
$sql = "SELECT n.*, c.name as category_name, c.color as category_color,
(SELECT GROUP_CONCAT(t.name) FROM note_tags nt
JOIN tags t ON nt.tag_id = t.id WHERE nt.note_id = n.id) as tags
FROM notes n
LEFT JOIN categories c ON n.category_id = c.id
WHERE n.user_id = :user_id";
$params = ['user_id' => $userId];
// Apply filters
if (!empty($filters['category_id'])) {
$sql .= " AND n.category_id = :category_id";
$params['category_id'] = $filters['category_id'];
}
if (isset($filters['is_favorite'])) {
$sql .= " AND n.is_favorite = :is_favorite";
$params['is_favorite'] = $filters['is_favorite'];
}
if (isset($filters['is_archived'])) {
$sql .= " AND n.is_archived = :is_archived";
$params['is_archived'] = $filters['is_archived'];
}
if (isset($filters['is_trashed'])) {
$sql .= " AND n.is_trashed = :is_trashed";
$params['is_trashed'] = $filters['is_trashed'];
} else {
$sql .= " AND n.is_trashed = 0";
}
if (isset($filters['is_pinned'])) {
$sql .= " AND n.is_pinned = :is_pinned";
$params['is_pinned'] = $filters['is_pinned'];
}
if (!empty($filters['tag'])) {
$sql .= " AND n.id IN (SELECT note_id FROM note_tags nt
JOIN tags t ON nt.tag_id = t.id
WHERE t.name = :tag)";
$params['tag'] = $filters['tag'];
}
if (!empty($filters['search'])) {
$sql .= " AND (n.title LIKE :search OR n.content LIKE :search OR n.content_plain LIKE :search)";
$params['search'] = '%' . $filters['search'] . '%';
}
// Sorting
$sort = $filters['sort'] ?? 'updated_at';
$order = $filters['order'] ?? 'DESC';
$allowedSorts = ['title', 'created_at', 'updated_at', 'views', 'word_count'];
if (in_array($sort, $allowedSorts)) {
$sql .= " ORDER BY n.{$sort} {$order}";
} else {
$sql .= " ORDER BY n.is_pinned DESC, n.updated_at DESC";
}
// Pagination
$page = $filters['page'] ?? 1;
$limit = $filters['limit'] ?? ITEMS_PER_PAGE;
$offset = ($page - 1) * $limit;
$sql .= " LIMIT :limit OFFSET :offset";
$params['limit'] = $limit;
$params['offset'] = $offset;
return $this->db->getRows($sql, $params);
}
/**
* Get notes count
*/
public function getNotesCount($userId, $filters = []) {
$sql = "SELECT COUNT(*) FROM notes WHERE user_id = :user_id";
$params = ['user_id' => $userId];
if (!empty($filters['category_id'])) {
$sql .= " AND category_id = :category_id";
$params['category_id'] = $filters['category_id'];
}
if (isset($filters['is_favorite'])) {
$sql .= " AND is_favorite = :is_favorite";
$params['is_favorite'] = $filters['is_favorite'];
}
if (isset($filters['is_archived'])) {
$sql .= " AND is_archived = :is_archived";
$params['is_archived'] = $filters['is_archived'];
}
if (isset($filters['is_trashed'])) {
$sql .= " AND is_trashed = :is_trashed";
$params['is_trashed'] = $filters['is_trashed'];
} else {
$sql .= " AND is_trashed = 0";
}
return $this->db->getValue($sql, $params);
}
/**
* Search notes
*/
public function searchNotes($userId, $query) {
return $this->db->getRows(
"SELECT n.*, c.name as category_name, c.color as category_color,
MATCH(n.title, n.content, n.content_plain) AGAINST(:query) as relevance
FROM notes n
LEFT JOIN categories c ON n.category_id = c.id
WHERE n.user_id = :user_id
AND n.is_trashed = 0
AND MATCH(n.title, n.content, n.content_plain) AGAINST(:query IN NATURAL LANGUAGE MODE)
ORDER BY relevance DESC",
[
'user_id' => $userId,
'query' => $query
]
);
}
/**
* Get note versions
*/
public function getVersions($noteId, $userId) {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return [];
}
return $this->db->getRows(
"SELECT v.*, u.username as created_by_username
FROM note_versions v
LEFT JOIN users u ON v.created_by = u.id
WHERE v.note_id = ?
ORDER BY v.version DESC",
[$noteId]
);
}
/**
* Restore version
*/
public function restoreVersion($noteId, $version, $userId) {
try {
$version = $this->db->getRow(
"SELECT * FROM note_versions WHERE note_id = ? AND version = ?",
[$noteId, $version]
);
if (!$version) {
return ['success' => false, 'error' => 'Version not found'];
}
$this->db->beginTransaction();
// Save current version
$note = $this->getNoteById($noteId, $userId);
$this->saveVersion($noteId, $note['title'], $note['content'], $userId);
// Restore version
$this->db->update(
'notes',
[
'title' => $version['title'],
'content' => $version['content'],
'content_plain' => strip_tags($version['content']),
'word_count' => $version['word_count'],
'character_count' => $version['character_count']
],
'id = :id',
['id' => $noteId]
);
$this->db->commit();
logAudit($userId, 'restore_version', 'Restored note version', ['note_id' => $noteId, 'version' => $version['version']]);
return ['success' => true, 'message' => 'Version restored successfully'];
} catch (Exception $e) {
$this->db->rollback();
logError('Restore version error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to restore version'];
}
}
/**
* Get note attachments
*/
public function getAttachments($noteId, $userId) {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return [];
}
return $this->db->getRows(
"SELECT * FROM attachments WHERE note_id = ? ORDER BY created_at DESC",
[$noteId]
);
}
/**
* Get note reminders
*/
public function getReminders($noteId, $userId) {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return [];
}
return $this->db->getRows(
"SELECT * FROM reminders WHERE note_id = ? ORDER BY reminder_time ASC",
[$noteId]
);
}
/**
* Increment view count
*/
public function incrementViewCount($noteId) {
$this->db->query(
"UPDATE notes SET views = views + 1 WHERE id = ?",
[$noteId]
);
}
/**
* Duplicate note
*/
public function duplicate($noteId, $userId) {
try {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return ['success' => false, 'error' => 'Note not found'];
}
$this->db->beginTransaction();
// Create duplicate
$duplicateData = [
'title' => $note['title'] . ' (Copy)',
'content' => $note['content'],
'category_id' => $note['category_id'],
'editor_type' => $note['editor_type'],
'color' => $note['color'],
'icon' => $note['icon']
];
$result = $this->create($userId, $duplicateData);
if (!$result['success']) {
throw new Exception('Failed to duplicate note');
}
// Duplicate tags
$tags = $this->db->getRows(
"SELECT t.name FROM note_tags nt
JOIN tags t ON nt.tag_id = t.id
WHERE nt.note_id = ?",
[$noteId]
);
if (!empty($tags)) {
$tagNames = array_column($tags, 'name');
$this->addTags($result['note_id'], $userId, $tagNames);
}
$this->db->commit();
logAudit($userId, 'duplicate_note', 'Duplicated note', ['original_note_id' => $noteId, 'new_note_id' => $result['note_id']]);
return ['success' => true, 'note_id' => $result['note_id'], 'message' => 'Note duplicated successfully'];
} catch (Exception $e) {
$this->db->rollback();
logError('Duplicate note error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to duplicate note'];
}
}
/**
* Export note
*/
public function exportNote($noteId, $userId, $format = 'html') {
$note = $this->getNoteById($noteId, $userId);
if (!$note) {
return ['success' => false, 'error' => 'Note not found'];
}
switch ($format) {
case 'html':
return $this->exportToHtml($note);
case 'markdown':
return $this->exportToMarkdown($note);
case 'text':
return $this->exportToText($note);
case 'pdf':
return $this->exportToPdf($note);
default:
return ['success' => false, 'error' => 'Invalid export format'];
}
}
/**
* Export to HTML
*/
private function exportToHtml($note) {
$html = '<!DOCTYPE html>';
$html .= '<html><head><title>' . htmlspecialchars($note['title']) . '</title>';
$html .= '<meta charset="utf-8"></head><body>';
$html .= '<h1>' . htmlspecialchars($note['title']) . '</h1>';
$html .= '<p><small>Created: ' . $note['created_at'] . ' | Last updated: ' . $note['updated_at'] . '</small></p>';
$html .= '<hr>';
$html .= $note['content'];
$html .= '</body></html>';
$filename = 'note_' . $note['id'] . '_' . date('Ymd') . '.html';
$filepath = UPLOAD_DIR . 'exports/' . $filename;
file_put_contents($filepath, $html);
return [
'success' => true,
'filename' => $filename,
'filepath' => $filepath,
'url' => SITE_URL . '/uploads/exports/' . $filename
];
}
/**
* Export to Markdown
*/
private function exportToMarkdown($note) {
$content = '# ' . $note['title'] . "\n\n";
$content .= '*Created: ' . $note['created_at'] . ' | Last updated: ' . $note['updated_at'] . "*\n\n";
$content .= '---' . "\n\n";
// Convert HTML to Markdown (simplified)
$markdown = strip_tags($note['content']);
$content .= $markdown;
$filename = 'note_' . $note['id'] . '_' . date('Ymd') . '.md';
$filepath = UPLOAD_DIR . 'exports/' . $filename;
file_put_contents($filepath, $content);
return [
'success' => true,
'filename' => $filename,
'filepath' => $filepath,
'url' => SITE_URL . '/uploads/exports/' . $filename
];
}
/**
* Export to plain text
*/
private function exportToText($note) {
$content = $note['title'] . "\n";
$content .= str_repeat('=', strlen($note['title'])) . "\n\n";
$content .= 'Created: ' . $note['created_at'] . "\n";
$content .= 'Last updated: ' . $note['updated_at'] . "\n\n";
$content .= strip_tags($note['content']);
$filename = 'note_' . $note['id'] . '_' . date('Ymd') . '.txt';
$filepath = UPLOAD_DIR . 'exports/' . $filename;
file_put_contents($filepath, $content);
return [
'success' => true,
'filename' => $filename,
'filepath' => $filepath,
'url' => SITE_URL . '/uploads/exports/' . $filename
];
}
/**
* Export to PDF (placeholder)
*/
private function exportToPdf($note) {
// This would use TCPDF or similar library
// Placeholder for PDF export
return ['success' => false, 'error' => 'PDF export not implemented'];
}
}
?>
Frontend Pages
Main Landing Page
File: index.php
<?php
require_once 'includes/config.php';
// Redirect to dashboard if already logged in
if ($auth->isLoggedIn()) {
redirect('/user/dashboard.php');
}
// Get stats
$totalUsers = $db->getValue("SELECT COUNT(*) FROM users WHERE status = 'active'");
$totalNotes = $db->getValue("SELECT COUNT(*) FROM notes WHERE is_trashed = 0");
$totalPublicNotes = $db->getValue("SELECT COUNT(*) FROM notes WHERE is_public = 1 AND is_trashed = 0");
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo SITE_NAME; ?> - Your Personal Notes Taking App</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css">
<meta name="description" content="A powerful, easy-to-use notes taking application to capture your ideas, organize your thoughts, and boost your productivity.">
<meta name="keywords" content="notes, note taking, productivity, organization, ideas">
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm fixed-top">
<div class="container">
<a class="navbar-brand" href="index.php">
<i class="fas fa-sticky-note text-primary me-2"></i>
<?php echo SITE_NAME; ?>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link active" href="index.php">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#features">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#pricing">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#about">About</a>
</li>
<li class="nav-item">
<a class="btn btn-outline-primary me-2" href="login.php">Login</a>
</li>
<li class="nav-item">
<a class="btn btn-primary" href="register.php">Sign Up Free</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="hero-section bg-primary text-white py-5 mt-5">
<div class="container py-5">
<div class="row align-items-center">
<div class="col-lg-6">
<h1 class="display-4 fw-bold mb-4">Capture Your Ideas, Organize Your Thoughts</h1>
<p class="lead mb-4">The most intuitive and powerful notes taking app to help you stay organized and boost your productivity.</p>
<div class="d-flex gap-3">
<a href="register.php" class="btn btn-light btn-lg px-4">
<i class="fas fa-user-plus me-2"></i>Get Started Free
</a>
<a href="#features" class="btn btn-outline-light btn-lg px-4">
Learn More
</a>
</div>
<div class="mt-4">
<div class="row text-center">
<div class="col-4">
<h3 class="fw-bold"><?php echo number_format($totalUsers); ?></h3>
<small>Happy Users</small>
</div>
<div class="col-4">
<h3 class="fw-bold"><?php echo number_format($totalNotes); ?></h3>
<small>Notes Created</small>
</div>
<div class="col-4">
<h3 class="fw-bold"><?php echo number_format($totalPublicNotes); ?></h3>
<small>Public Notes</small>
</div>
</div>
</div>
</div>
<div class="col-lg-6 text-center">
<img src="assets/images/hero-notes.svg" alt="Notes App" class="img-fluid">
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="py-5">
<div class="container">
<h2 class="text-center fw-bold mb-5">Why Choose Our Notes App?</h2>
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-pencil-alt"></i>
</div>
<h4>Rich Text Editing</h4>
<p class="text-muted">Format your notes with bold, italic, lists, headings, and more using our powerful editor.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-folder-tree"></i>
</div>
<h4>Organize with Categories</h4>
<p class="text-muted">Keep your notes organized with custom categories, tags, and folders.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-search"></i>
</div>
<h4>Powerful Search</h4>
<p class="text-muted">Find any note instantly with our full-text search across titles and content.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<h4>Cloud Sync</h4>
<p class="text-muted">Access your notes from anywhere, on any device. Your data is always in sync.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-share-alt"></i>
</div>
<h4>Easy Sharing</h4>
<p class="text-muted">Share your notes with others, collaborate, and get feedback.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-center p-4">
<div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3">
<i class="fas fa-lock"></i>
</div>
<h4>Secure & Private</h4>
<p class="text-muted">Your notes are encrypted and secure. We take your privacy seriously.</p>
</div>
</div>
</div>
</div>
</section>
<!-- How It Works -->
<section class="py-5 bg-light">
<div class="container">
<h2 class="text-center fw-bold mb-5">How It Works</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="step-card text-center">
<div class="step-number">1</div>
<i class="fas fa-user-plus fa-3x text-primary mb-3"></i>
<h4>Create Account</h4>
<p class="text-muted">Sign up for free in less than a minute.</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="step-card text-center">
<div class="step-number">2</div>
<i class="fas fa-pen fa-3x text-primary mb-3"></i>
<h4>Start Writing</h4>
<p class="text-muted">Create notes, organize them into categories, and add tags.</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="step-card text-center">
<div class="step-number">3</div>
<i class="fas fa-tachometer-alt fa-3x text-primary mb-3"></i>
<h4>Stay Productive</h4>
<p class="text-muted">Access your notes anywhere, set reminders, and never forget an idea.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Pricing Section -->
<section id="pricing" class="py-5">
<div class="container">
<h2 class="text-center fw-bold mb-5">Simple, Transparent Pricing</h2>
<div class="row justify-content-center">
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-white text-center py-4">
<h4 class="mb-0">Basic</h4>
<p class="display-4 fw-bold text-primary">$0</p>
<p class="text-muted">Free forever</p>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>100 MB storage</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Rich text editing</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Categories & tags</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Search notes</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Export to text/HTML</li>
<li class="mb-3 text-muted"><i class="fas fa-times text-danger me-2"></i>Version history</li>
<li class="mb-3 text-muted"><i class="fas fa-times text-danger me-2"></i>File attachments</li>
</ul>
</div>
<div class="card-footer bg-white border-0 pb-4 text-center">
<a href="register.php" class="btn btn-outline-primary btn-lg">Get Started</a>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-lg featured">
<div class="card-header bg-primary text-white text-center py-4">
<span class="badge bg-warning position-absolute top-0 end-0 m-3">Popular</span>
<h4 class="mb-0">Premium</h4>
<p class="display-4 fw-bold">$4.99</p>
<p class="mb-0">per month</p>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>1 GB storage</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>All Basic features</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>30-day version history</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>File attachments</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Export to PDF</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Priority support</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>No ads</li>
</ul>
</div>
<div class="card-footer bg-white border-0 pb-4 text-center">
<a href="register.php?plan=premium" class="btn btn-primary btn-lg">Upgrade Now</a>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Testimonials -->
<section class="py-5 bg-light">
<div class="container">
<h2 class="text-center fw-bold mb-5">What Our Users Say</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3 text-warning">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<p class="card-text">"This notes app has completely transformed how I organize my thoughts. The rich editor and category system are perfect for my needs."</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user1.jpg" class="rounded-circle me-3" width="50" alt="User">
<div>
<h6 class="mb-0">Sarah Johnson</h6>
<small class="text-muted">Writer</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3 text-warning">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<p class="card-text">"I use it for everything - meeting notes, project planning, and personal journaling. The search feature is incredibly fast and accurate."</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user2.jpg" class="rounded-circle me-3" width="50" alt="User">
<div>
<h6 class="mb-0">Michael Chen</h6>
<small class="text-muted">Product Manager</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body p-4">
<div class="mb-3 text-warning">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star-half-alt"></i>
</div>
<p class="card-text">"The premium features are worth every penny. Version history has saved me multiple times, and the PDF export is perfect for sharing."</p>
<div class="d-flex align-items-center">
<img src="assets/images/avatars/user3.jpg" class="rounded-circle me-3" width="50" alt="User">
<div>
<h6 class="mb-0">Emily Rodriguez</h6>
<small class="text-muted">Researcher</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Call to Action -->
<section class="py-5 bg-primary text-white">
<div class="container text-center py-4">
<h2 class="fw-bold mb-4">Ready to Start Taking Better Notes?</h2>
<p class="lead mb-4">Join thousands of happy users who have already organized their digital lives.</p>
<a href="register.php" class="btn btn-light btn-lg px-5">
<i class="fas fa-rocket me-2"></i>Get Started Free
</a>
</div>
</section>
<!-- Footer -->
<footer id="about" class="bg-dark text-white py-5">
<div class="container">
<div class="row">
<div class="col-md-4 mb-4">
<h5><i class="fas fa-sticky-note me-2"></i><?php echo SITE_NAME; ?></h5>
<p class="text-white-50">The most intuitive notes taking app to capture your ideas and boost your productivity.</p>
<div class="social-links">
<a href="#" class="text-white me-2"><i class="fab fa-facebook fa-lg"></i></a>
<a href="#" class="text-white me-2"><i class="fab fa-twitter fa-lg"></i></a>
<a href="#" class="text-white me-2"><i class="fab fa-instagram fa-lg"></i></a>
<a href="#" class="text-white me-2"><i class="fab fa-linkedin fa-lg"></i></a>
</div>
</div>
<div class="col-md-2 mb-4">
<h6>Quick Links</h6>
<ul class="list-unstyled">
<li><a href="index.php" class="text-white-50">Home</a></li>
<li><a href="#features" class="text-white-50">Features</a></li>
<li><a href="#pricing" class="text-white-50">Pricing</a></li>
<li><a href="#about" class="text-white-50">About</a></li>
</ul>
</div>
<div class="col-md-3 mb-4">
<h6>Legal</h6>
<ul class="list-unstyled">
<li><a href="#" class="text-white-50">Terms of Service</a></li>
<li><a href="#" class="text-white-50">Privacy Policy</a></li>
<li><a href="#" class="text-white-50">Cookie Policy</a></li>
<li><a href="#" class="text-white-50">GDPR</a></li>
</ul>
</div>
<div class="col-md-3 mb-4">
<h6>Contact</h6>
<ul class="list-unstyled text-white-50">
<li><i class="fas fa-envelope me-2"></i><?php echo SITE_EMAIL; ?></li>
<li><i class="fas fa-globe me-2"></i><?php echo SITE_URL; ?></li>
</ul>
</div>
</div>
<hr class="border-secondary">
<div class="row">
<div class="col-12 text-center">
<p class="text-white-50 mb-0">© <?php echo date('Y'); ?> <?php echo SITE_NAME; ?>. All rights reserved.</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js"></script>
<style>
.feature-icon {
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
}
.step-card {
position: relative;
padding: 30px 20px;
background: white;
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0,0,0,0.05);
}
.step-number {
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 40px;
background: var(--bs-primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2rem;
}
.navbar {
padding: 1rem 0;
}
.hero-section {
margin-top: 76px;
}
.featured {
transform: scale(1.05);
z-index: 1;
}
</style>
</body>
</html>
Environment Configuration
File: .env
# Database Configuration DB_HOST=localhost DB_NAME=notes_app DB_USER=root DB_PASS= # Application Configuration SITE_NAME=Notes App SITE_URL=http://localhost/notes-app [email protected] APP_VERSION=1.0.0 DEBUG_MODE=true # Security SESSION_TIMEOUT=3600 BCRYPT_ROUNDS=12 PEPPER=your-secret-pepper-string-change-this # Upload Configuration MAX_FILE_SIZE=5242880 # 5MB in bytes ALLOWED_FILE_TYPES=jpg,jpeg,png,gif,pdf,doc,docx,txt,md # Storage Limits (in bytes) BASIC_STORAGE_LIMIT=104857600 # 100 MB PREMIUM_STORAGE_LIMIT=1073741824 # 1 GB # Pagination ITEMS_PER_PAGE=20 # Date/Time TIMEZONE=America/New_York DATE_FORMAT=Y-m-d TIME_FORMAT=H:i # User Settings ALLOW_REGISTRATION=true REQUIRE_EMAIL_VERIFICATION=true DEFAULT_USER_ROLE=basic # Feature Flags ENABLE_RICH_EDITOR=true ENABLE_MARKDOWN=true ENABLE_ATTACHMENTS=true ENABLE_SHARING=true ENABLE_REMINDERS=true # Mail Configuration MAIL_DRIVER=smtp MAIL_HOST=smtp.gmail.com MAIL_PORT=587 MAIL_USERNAME= MAIL_PASSWORD= MAIL_ENCRYPTION=tls [email protected] MAIL_FROM_NAME=Notes App
File: .gitignore
# Environment variables .env # Dependencies /vendor/ node_modules/ # IDE files .vscode/ .idea/ *.sublime-* # OS files .DS_Store Thumbs.db # Logs /logs/ *.log # Uploads /uploads/ !/uploads/.gitkeep !/uploads/avatars/.gitkeep !/uploads/note-images/.gitkeep !/uploads/attachments/.gitkeep !/uploads/exports/.gitkeep # Cache /cache/ !/cache/.gitkeep # Composer composer.lock # Temp files *.tmp *.temp
File: composer.json
{
"name": "notes-app/application",
"description": "Notes Taking Application",
"type": "project",
"require": {
"php": ">=7.4",
"ext-pdo": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-curl": "*",
"ext-gd": "*",
"phpmailer/phpmailer": "^6.8",
"tecnickcom/tcpdf": "^6.6",
"erusev/parsedown": "^1.7",
"intervention/image": "^2.7"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"NotesApp\\": "src/"
}
},
"scripts": {
"test": "phpunit tests",
"post-install-cmd": [
"chmod -R 755 uploads/",
"chmod -R 755 logs/",
"chmod -R 755 cache/"
]
}
}
How to Use the Project (Step-by-Step Guide)
Prerequisites
- Web Server: XAMPP, WAMP, MAMP, or any PHP-enabled server (PHP 7.4+)
- Database: MySQL 5.7+ or MariaDB
- Composer: For dependency management
- Browser: Modern web browser (Chrome, Firefox, Edge, etc.)
Installation Steps
Step 1: Set Up Local Server
- Download and install XAMPP from https://www.apachefriends.org/
- Launch XAMPP Control Panel
- Start Apache and MySQL services
Step 2: Install Composer Dependencies
- Download and install Composer from https://getcomposer.org/
- Navigate to your project directory in terminal
- Run:
composer install
Step 3: Create Project Folder
- Navigate to
C:\xampp\htdocs\(Windows) or/Applications/XAMPP/htdocs/(Mac) - Create a new folder named
notes-app - Create all the folders and files as shown in the Project File Structure
Step 4: Set Up Database
- Open browser and go to
http://localhost/phpmyadmin - Click on "New" to create a new database
- Name the database
notes_appand selectutf8mb4_general_ci - Click on "Import" tab
- Click "Choose File" and select the
database.sqlfile from thesqlfolder - Click "Go" to import the database structure and sample data
Step 5: Configure Environment
- Rename
.env.exampleto.envin the project root - Update database credentials:
DB_HOST=localhost DB_NAME=notes_app DB_USER=root DB_PASS=
- Update application URL:
SITE_URL=http://localhost/notes-app
- Set a secure pepper string:
PEPPER=your-random-secure-string-here-min-32-characters
- Configure email settings if using email notifications
Step 6: Set Folder Permissions
Create the following folders and ensure they are writable:
uploads/avatars/uploads/note-images/uploads/attachments/uploads/exports/logs/cache/
On Windows, right-click folders → Properties → Security → give Write permission to Users
On Mac/Linux, run: chmod -R 755 uploads/ logs/ cache/
Step 7: Create Admin Password Hash
- Open a PHP file or use an online tool to generate a password hash:
<?php $password = 'Admin@123'; $peppered = $password . 'your-pepper-string'; echo password_hash($peppered, PASSWORD_BCRYPT, ['cost' => 12]); ?>
- Copy the generated hash
- Open phpMyAdmin, go to the
userstable - Insert or update the admin user with the hash
Step 8: Test the Installation
- Open browser and go to
http://localhost/notes-app/ - You should see the landing page
- Test different user types: Admin Login:
- Username:
admin(or email:[email protected]) - Password:
Admin@123(or the password you set) Regular User Registration: - Click "Sign Up Free" and register
- Verify email (if enabled)
- Login and start creating notes
System Walkthrough
For Regular Users:
- Dashboard - Overview of your notes and recent activity
- Notes - View all notes with filtering options
- Create Note - Create new notes with rich text editor
- Categories - Manage categories/folders
- Tags - Manage tags for better organization
- Favorites - View favorite notes
- Archive - View archived notes
- Trash - Recover deleted notes
- Search - Search across all notes
- Profile - Update personal information and settings
- Upgrade - Upgrade to premium plan
For Premium Users (additional features):
- Version History - View and restore previous versions
- Attachments - Upload and manage file attachments
- Export Options - Export notes to PDF
- Reminders - Set reminders on notes
For Admins:
- Dashboard - System-wide statistics and overview
- User Management - Manage users, roles, and permissions
- Storage Stats - Monitor storage usage
- System Settings - Configure system parameters
- Backup - Database backup and restore
- Logs - View system and error logs
- Analytics - View usage analytics
Key Features Explained
Rich Text Editor
The application uses TinyMCE or CKEditor for rich text editing with features:
- Text formatting: Bold, italic, underline, strikethrough
- Headings: H1, H2, H3
- Lists: Ordered and unordered
- Links: Insert and edit hyperlinks
- Images: Upload and embed images
- Tables: Create and edit tables
- Code blocks: For code snippets
- Blockquotes: For quotations
- Undo/Redo: Full history support
Organization System
- Categories: Create hierarchical folders to organize notes
- Tags: Add multiple tags to notes for cross-categorization
- Favorites: Mark important notes for quick access
- Pinned: Keep important notes at the top
- Archive: Move old notes out of the way without deleting
- Trash: Recover deleted notes within 30 days
Search Functionality
- Full-text search: Search across titles and content
- Filter by category: Narrow down by category
- Filter by tags: Find notes with specific tags
- Filter by date: Search by creation or update date
- Filter by status: Search in favorites, archive, or trash
Sharing (Premium)
- Share with users: Share notes with other registered users
- Public links: Generate public shareable links
- Permissions: View, comment, or edit permissions
- Expiration: Set expiration dates for shared links
- Password protection: Protect shared notes with password
Version History (Premium)
- Auto-save: Versions saved every 5 minutes
- Manual saves: Save versions manually
- 30-day history: Access versions from last 30 days
- Compare: Compare different versions
- Restore: Restore any previous version
Attachments (Premium)
- File uploads: Attach files to notes
- Image gallery: View all images in a note
- File types: PDF, DOC, DOCX, images, and more
- Storage tracking: Monitor storage usage
- Download: Download attachments
Reminders (Premium)
- Set reminders: Add reminders to notes
- Due dates: Set specific dates and times
- Recurring: Daily, weekly, monthly, yearly
- Email notifications: Get email reminders
- Push notifications: Browser push notifications
Troubleshooting
Common Issues and Solutions
- Database Connection Error
- Check if MySQL is running
- Verify database credentials in
.env - Ensure database
notes_appexists
- 404 Page Not Found
- Check file paths and folder structure
- Verify
SITE_URLin.env - Ensure
.htaccessis properly configured (if using Apache)
- Upload Issues
- Check folder permissions (755 or 777)
- Verify
MAX_FILE_SIZEin.env - Check allowed file extensions
- Email Not Sending
- Configure SMTP settings correctly
- Check spam folder
- Verify PHP mail() function is enabled
- Session/Login Issues
- Clear browser cookies and cache
- Check
SESSION_TIMEOUTin.env - Verify session save path is writable
- Rich Editor Not Loading
- Check TinyMCE/CKEditor paths
- Verify JavaScript console for errors
- Ensure editor files are in
assets/plugins/
- Search Not Working
- Verify MySQL FULLTEXT indexes
- Check if InnoDB engine is used
- Ensure tables are using utf8mb4
Performance Optimizations
- Database Indexing on frequently queried columns
- Query Caching for repeated requests
- Image Optimization for uploads
- Lazy Loading for images and content
- Pagination for large datasets
- Minified CSS and JavaScript for production
- CDN for static assets
- Browser Caching headers
- Gzip Compression for faster loading
Security Best Practices
- Change default admin password immediately after installation
- Use HTTPS in production with SSL certificate
- Regular backups of database and uploads
- Input validation on both client and server side
- SQL injection prevention using prepared statements
- XSS prevention using
htmlspecialchars() - CSRF tokens for all forms
- Password hashing with bcrypt and pepper
- Rate limiting for login attempts
- Session security with proper timeout
- File upload validation and malware scanning
- CSP headers for additional security
Deployment to Production
- Update
.envwith production settings - Set DEBUG_MODE=false
- Configure proper error logging
- Set up SSL certificate for HTTPS
- Configure cron jobs for maintenance tasks:
- Daily backups
- Clean up expired sessions
- Send reminder emails
- Delete old trashed notes
- Set up database backups regularly
- Enable CDN for static assets
- Configure caching (Redis/Memcached)
- Set up monitoring and alerts
- Configure firewall and security rules
Conclusion
The Notes Taking App with Database is a comprehensive, feature-rich application for capturing, organizing, and managing digital notes. With its intuitive interface, powerful rich text editor, robust organization system, and premium features, it provides everything needed for effective note-taking and information management.
This application demonstrates:
- Secure user authentication with password hashing and pepper
- CRUD operations for notes, categories, and tags
- Rich text editing with TinyMCE/CKEditor
- Full-text search capabilities
- File upload and attachment management
- Version history for premium users
- Sharing and collaboration features
- Reminders and notifications
- Responsive design for all devices
- Modular code structure following OOP principles
- Database design with proper relationships and indexing
- Security best practices throughout
- Premium tier with additional features
The system is built to be extensible, allowing easy addition of new features like collaborative editing, mobile apps, API integration, and more. Whether you're a student taking lecture notes, a professional organizing project ideas, a writer collecting research, or a team collaborating on documents, this notes app provides a solid foundation that can be customized to meet specific needs.
With proper deployment, regular backups, and security updates, this application can serve as a reliable tool for personal and professional note-taking for years to come.