Notes Taking App with Database IN HTML CSS JAVASCRIPT WITH PHP AND MY SQL

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">&copy; <?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

  1. Web Server: XAMPP, WAMP, MAMP, or any PHP-enabled server (PHP 7.4+)
  2. Database: MySQL 5.7+ or MariaDB
  3. Composer: For dependency management
  4. Browser: Modern web browser (Chrome, Firefox, Edge, etc.)

Installation Steps

Step 1: Set Up Local Server

  1. Download and install XAMPP from https://www.apachefriends.org/
  2. Launch XAMPP Control Panel
  3. Start Apache and MySQL services

Step 2: Install Composer Dependencies

  1. Download and install Composer from https://getcomposer.org/
  2. Navigate to your project directory in terminal
  3. Run: composer install

Step 3: Create Project Folder

  1. Navigate to C:\xampp\htdocs\ (Windows) or /Applications/XAMPP/htdocs/ (Mac)
  2. Create a new folder named notes-app
  3. Create all the folders and files as shown in the Project File Structure

Step 4: Set Up Database

  1. Open browser and go to http://localhost/phpmyadmin
  2. Click on "New" to create a new database
  3. Name the database notes_app and select utf8mb4_general_ci
  4. Click on "Import" tab
  5. Click "Choose File" and select the database.sql file from the sql folder
  6. Click "Go" to import the database structure and sample data

Step 5: Configure Environment

  1. Rename .env.example to .env in the project root
  2. Update database credentials:
   DB_HOST=localhost
DB_NAME=notes_app
DB_USER=root
DB_PASS=
  1. Update application URL:
   SITE_URL=http://localhost/notes-app
  1. Set a secure pepper string:
   PEPPER=your-random-secure-string-here-min-32-characters
  1. 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

  1. 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]);
?>
  1. Copy the generated hash
  2. Open phpMyAdmin, go to the users table
  3. Insert or update the admin user with the hash

Step 8: Test the Installation

  1. Open browser and go to http://localhost/notes-app/
  2. You should see the landing page
  3. 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:

  1. Dashboard - Overview of your notes and recent activity
  2. Notes - View all notes with filtering options
  3. Create Note - Create new notes with rich text editor
  4. Categories - Manage categories/folders
  5. Tags - Manage tags for better organization
  6. Favorites - View favorite notes
  7. Archive - View archived notes
  8. Trash - Recover deleted notes
  9. Search - Search across all notes
  10. Profile - Update personal information and settings
  11. Upgrade - Upgrade to premium plan

For Premium Users (additional features):

  1. Version History - View and restore previous versions
  2. Attachments - Upload and manage file attachments
  3. Export Options - Export notes to PDF
  4. Reminders - Set reminders on notes

For Admins:

  1. Dashboard - System-wide statistics and overview
  2. User Management - Manage users, roles, and permissions
  3. Storage Stats - Monitor storage usage
  4. System Settings - Configure system parameters
  5. Backup - Database backup and restore
  6. Logs - View system and error logs
  7. 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

  1. Database Connection Error
  • Check if MySQL is running
  • Verify database credentials in .env
  • Ensure database notes_app exists
  1. 404 Page Not Found
  • Check file paths and folder structure
  • Verify SITE_URL in .env
  • Ensure .htaccess is properly configured (if using Apache)
  1. Upload Issues
  • Check folder permissions (755 or 777)
  • Verify MAX_FILE_SIZE in .env
  • Check allowed file extensions
  1. Email Not Sending
  • Configure SMTP settings correctly
  • Check spam folder
  • Verify PHP mail() function is enabled
  1. Session/Login Issues
  • Clear browser cookies and cache
  • Check SESSION_TIMEOUT in .env
  • Verify session save path is writable
  1. Rich Editor Not Loading
  • Check TinyMCE/CKEditor paths
  • Verify JavaScript console for errors
  • Ensure editor files are in assets/plugins/
  1. Search Not Working
  • Verify MySQL FULLTEXT indexes
  • Check if InnoDB engine is used
  • Ensure tables are using utf8mb4

Performance Optimizations

  1. Database Indexing on frequently queried columns
  2. Query Caching for repeated requests
  3. Image Optimization for uploads
  4. Lazy Loading for images and content
  5. Pagination for large datasets
  6. Minified CSS and JavaScript for production
  7. CDN for static assets
  8. Browser Caching headers
  9. Gzip Compression for faster loading

Security Best Practices

  1. Change default admin password immediately after installation
  2. Use HTTPS in production with SSL certificate
  3. Regular backups of database and uploads
  4. Input validation on both client and server side
  5. SQL injection prevention using prepared statements
  6. XSS prevention using htmlspecialchars()
  7. CSRF tokens for all forms
  8. Password hashing with bcrypt and pepper
  9. Rate limiting for login attempts
  10. Session security with proper timeout
  11. File upload validation and malware scanning
  12. CSP headers for additional security

Deployment to Production

  1. Update .env with production settings
  2. Set DEBUG_MODE=false
  3. Configure proper error logging
  4. Set up SSL certificate for HTTPS
  5. Configure cron jobs for maintenance tasks:
  • Daily backups
  • Clean up expired sessions
  • Send reminder emails
  • Delete old trashed notes
  1. Set up database backups regularly
  2. Enable CDN for static assets
  3. Configure caching (Redis/Memcached)
  4. Set up monitoring and alerts
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper