A clean, modern, single-page todo app built with PHP, MySQL & Vanilla JS
Add tasks · Edit inline · Mark complete · Delete · All without page reload.
| Feature | Description | |
|---|---|---|
| ➕ | Add Task | Type a title and press Enter or click Add Task |
| ✏️ | Inline Edit | Click the pen icon — title becomes an input field; save with Enter, cancel with Esc |
| ✅ | Mark Complete | One click moves the task from Pending → Completed list |
| 🗑️ | Delete Task | Confirmation-guarded hard delete |
| 🔔 | Toast Feedback | Non-blocking success/error notifications on every action |
| 📱 | Responsive | Mobile-first layout, stacks cleanly on narrow screens |
| 🔒 | Secure | Prepared statements, input sanitisation, XSS-safe output |
tasky/
├── index.php # Main UI — HTML, CSS design system, JS AJAX controller
├── api.php # JSON API endpoint — CRUD routing via PDO
├── setup.sql # One-time DB + table creation script
└── config/
└── db.php # PDO connection factory (getDbConnection)
| Layer | Technology |
|---|---|
| Backend | PHP 8.x |
| Database | MySQL 8.x |
| Frontend | HTML5 + CSS3 (custom properties, animations) |
| JavaScript | Vanilla ES2022 — Fetch API |
| Fonts | Syne + DM Sans via Google Fonts |
| Icons | Font Awesome 6.5 via CDN |
Database: todo_list | Table: tasks
CREATE TABLE `tasks` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`status` ENUM('pending', 'completed') NOT NULL DEFAULT 'pending',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;- PHP 8.x with
pdo_mysqlextension enabled - MySQL 8.x running locally
- Git (optional)
git clone https://github.com/YOUR_USERNAME/tasky.git
cd taskyOpen config/db.php and set your credentials:
define('DB_HOST', 'localhost');
define('DB_NAME', 'todo_list');
define('DB_USER', 'root');
define('DB_PASS', 'your_password_here'); // ← change this
define('DB_CHARSET', 'utf8mb4');
⚠️ Never commit real credentials. Addconfig/db.phpto.gitignoreon public repos.
mysql -u root -p < setup.sqlOr inside the MySQL shell:
SOURCE /path/to/tasky/setup.sql;php -S localhost:8080http://localhost:8080
All requests are HTTP POST with a JSON body to api.php.
Every response follows the envelope:
{ "success": true, "data": { ... }, "message": "..." }| Action | Payload | Description |
|---|---|---|
list |
— | Returns all tasks — pending first, then completed |
add |
{ "title": "…" } |
Inserts a new pending task; returns the created row |
edit |
{ "id": 1, "title": "…" } |
Updates the task title |
complete |
{ "id": 1 } |
Sets status → completed |
delete |
{ "id": 1 } |
Hard-deletes the task row |
Example request:
const res = await fetch('api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'add', title: 'Buy groceries' }),
});
const json = await res.json();
// { success: true, data: { id: 1, title: "Buy groceries", status: "pending", ... } }The frontend holds no local state — after every mutation, fetchTasks() re-renders both lists from the server.
Page Load
└─ fetchTasks() → POST api.php { action: "list" }
└─ renders #pending-list + #completed-list
User: Add / Complete / Delete
└─ api(action, payload) → POST api.php
└─ success → fetchTasks() → both lists refresh
User: Inline Edit
└─ startEdit() → DOM swap: title div → <input> [no request]
└─ saveEdit() → api("edit", {id, title}) → fetchTasks()
└─ cancelEdit() → fetchTasks() (restore from server)
- Prepared statements — all queries use PDO named placeholders (
:id,:title) - Input sanitisation —
strip_tags()+ whitespace normalisation on every title - Integer validation — IDs pass through
FILTER_VALIDATE_INT, never raw-cast - XSS prevention — all output in the browser is escaped via
escHtml() - Error safety — raw PDO exceptions are logged server-side; clients receive only a generic message
- HTTP headers —
Content-Type: application/json+X-Content-Type-Options: nosniff
| Problem | Fix |
|---|---|
JSON.parse error |
Open api.php?action=list directly in browser. Remove any stray echo from config/db.php |
A database error occurred |
Verify DB_PASS matches your MySQL password. Check MySQL service is running |
Class "PDO" not found |
Uncomment extension=pdo_mysql in php.ini, restart server |
Unknown database 'todo_list' |
Re-run setup.sql |
php not recognised (Windows) |
Add C:\php to system PATH, open a new terminal |
| Port 8080 in use | Use another port: php -S localhost:9090 |
# Exclude DB credentials from version control
config/db.php
# OS & editor noise
.DS_Store
Thumbs.db
.vscode/
*.logThis project is licensed under the MIT License — see the LICENSE file for details.
Made with ☕ and PHP · Tasky v1.0