Every file in this project explained line by line.
- 1. How All Files Work Together — The Big Picture
- 2. server.js — The Backend Server (Brain of the App)
- 3. database.sql — Database Setup Script
- 4. package.json — Project Configuration
- 5. package-lock.json — Dependency Lock File
- 6. .env.example — Environment Variables Template
- 7. .gitignore — Git Exclusions
- 8. public/index.html — The Web Page
- 9. public/script.js — Frontend Logic
- 10. public/style.css — Visual Styling
- 11. README.md — Project Documentation
Before diving into individual files, here's how they all connect:
USER'S BROWSER EC2 SERVER
┌──────────────────┐
┌──────────────────────────────────┐
│ │ │
│ index.html │ ── loads ──► │ server.js
│ (the page) │ │ (Express web server)
│ │ │ │ │
│ │ uses │ │ │ serves files from
│ ▼ │ │ │ public/ folder
│ style.css │ │ │
│ (appearance) │ │ │ provides API:
│ │ │ │ │ GET /api/expenses
│ │ loads │ │ │ POST /api/expenses
│ ▼ │ │ │ DELETE /api/expenses/:id
│ script.js ──────── API calls ──────────► │ │ DELETE /api/expenses
│ (browser logic) │ │ │
│ │ │ │ │ talks to
│ │ calls │ │ ▼
│ │ fetch() │ │ PostgreSQL Database
│ │ │ │ (database.sql created this)
│ ▼ │ │ │
│ Shows results │ ◄── JSON response ──── │ Returns data
│ in the table │ │
└──────────────────┘
└──────────────────────────────────┘
CONFIGURATION FILES (not running code):
┌───────────────────────────────────────────────────────────────┐
│ package.json → Tells npm what to install │
│ package-lock.json → Locks exact dependency versions │
│ .env.example → Template for secret configuration │
│ .gitignore → Tells Git what NOT to track │
│ README.md → Documentation for humans │
└───────────────────────────────────────────────────────────────┘
Every action in the app follows this cycle:
- User interacts with index.html (clicks button, submits form)
- script.js captures the event and makes an API call (fetch)
- The HTTP request travels over the internet to EC2
- server.js receives the request and routes it
- server.js talks to PostgreSQL (SQL query)
- PostgreSQL returns data
- server.js sends a JSON response back to the browser
- script.js receives the JSON and updates the HTML page
This is the most important file in the project — the entire backend in a single file. It runs on the EC2 server using Node.js.
// Simple Expense Tracker Server
// All code in one file for beginners!
const express = require("express");
const { Pool } = require("pg");
const path = require("path");
const app = express();
const PORT = 3000;
// ========== DATABASE SETUP ==========
// Change these values to match your PostgreSQL setup
const pool = new Pool({
user: "postgres", // Your PostgreSQL username
host: "localhost", // Database location
database: "expense_tracker", // Database name
password: "Obaid@pst123", // Your PostgreSQL password (CHANGE THIS!)
port: 5432, // PostgreSQL port
});
// Test database connection
pool.connect((err) => {
if (err) {
console.log("❌ Database connection failed!");
console.log("Error:", err.message);
} else {
console.log("✅ Database connected successfully!");
}
});
// ========== MIDDLEWARE ==========
app.use(express.json()); // Parse JSON data
app.use(express.static(path.join(__dirname, "public"))); // Serve HTML/CSS/JS files
// ========== ROUTES ==========
// Home page
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "public", "index.html"));
});
// Get all expenses
app.get("/api/expenses", async (req, res) => {
try {
const result = await pool.query(
"SELECT * FROM expenses ORDER BY date DESC"
);
res.json(result.rows);
} catch (err) {
console.log("Error:", err);
res.status(500).json({ error: "Failed to fetch expenses" });
}
});
// Add new expense
app.post("/api/expenses", async (req, res) => {
const { description, amount, category, date } = req.body;
// Check if all fields are filled
if (!description || !amount || !category || !date) {
return res.status(400).json({ error: "Please fill all fields" });
}
try {
const result = await pool.query(
"INSERT INTO expenses (description, amount, category, date) VALUES ($1, $2, $3, $4) RETURNING *",
[description, amount, category, date]
);
res.json(result.rows[0]);
} catch (err) {
console.log("Error:", err);
res.status(500).json({ error: "Failed to add expense" });
}
});
// Delete single expense by ID
app.delete("/api/expenses/:id", async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query(
"DELETE FROM expenses WHERE id = $1 RETURNING *",
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Expense not found" });
}
res.json({ message: "Expense deleted successfully" });
} catch (err) {
console.log("Error:", err);
res.status(500).json({ error: "Failed to delete expense" });
}
});
// Delete ALL expenses
app.delete("/api/expenses", async (req, res) => {
try {
await pool.query("DELETE FROM expenses");
res.json({ message: "All expenses deleted successfully" });
} catch (err) {
console.log("Error:", err);
res.status(500).json({ error: "Failed to delete all expenses" });
}
});
// ========== START SERVER ==========
app.listen(PORT, () => {
console.log("=================================");
console.log(`🚀 Server running on http://localhost:${PORT}`);
console.log("=================================");
});Lines 1-2: Comments
// Simple Expense Tracker Server
// All code in one file for beginners!Lines starting with // are comments — they're ignored by Node.js and exist only for human readers. Good practice to describe what a file does at the top.
Line 4:
require("express")Node.js's way of importing a module (library). It loads the Express.js framework from the node_modules folder (installed by npm install).
const express = require("express");const express — Stores the imported module in a constant variable. What is Express? A web framework that makes it easy to create an HTTP server, define URL routes, and handle requests/responses. Without Express, you'd need ~50 lines of boilerplate code just to create a basic server.
Line 5:
require("pg")Imports the pg (node-postgres) library that allows Node.js to communicate with PostgreSQL.
const { Pool } = require("pg");This is destructuring. The pg module exports multiple things; we only need the Pool class. It's equivalent to writing:
const pg = require("pg");
const Pool = pg.Pool;What is a Pool? A connection pool manages multiple database connections. Instead of opening a new connection for every query (slow: ~20-50ms per connection) and closing it after, the pool keeps connections alive and reuses them.
Line 6:
require("path")Imports Node.js's built-in path module. This is NOT installed by npm — it comes with Node.js itself.
What does it do? Helps construct file paths that work on any operating system (Windows uses \, Mac/Linux uses /). The path.join() method builds paths correctly regardless of OS.
Line 8:
const app = express();Calling Express as a function creates a new application instance. This object IS the web server. All routes, middleware, and configuration are added to this object. Think of app as the central hub — everything flows through it.
Line 9:
const PORT = 3000;Defines which port the server listens on. What is a port? A number that identifies a specific service on a machine. Like apartment numbers in a building — the building (server) has one address (IP), but each apartment (service) has its own number. Port 3000 is a common development port. Port 80 is the default HTTP port (no :80 needed in URLs). Port 443 is HTTPS.
Lines 13-19: Database Connection Pool
const pool = new Pool({
user: "postgres",
host: "localhost",
database: "expense_tracker",
password: "Obaid@pst123",
port: 5432,
});new Pool({...}) — Creates a new connection pool with the specified configuration. Configuration options:
| Option | Value | Meaning |
|---|---|---|
user |
"postgres" |
PostgreSQL username (default superuser) |
host |
"localhost" |
Where the database is running. localhost means the same machine as the server |
database |
"expense_tracker" |
The specific database name to connect to |
password |
"Obaid@pst123" |
The PostgreSQL user's password |
port |
5432 |
PostgreSQL's default port number |
Important security note: The password is hardcoded here, which is NOT a best practice. In production, you'd use environment variables (the .env.example file shows how).
Lines 22-29: Test Database Connection
pool.connect((err) => {
if (err) {
console.log("❌ Database connection failed!");
console.log("Error:", err.message);
} else {
console.log("✅ Database connected successfully!");
}
});pool.connect(callback) — Attempts to get a connection from the pool. This is a one-time test to verify the database is reachable when the server starts.
(err) => { ... } — A callback function. Node.js is asynchronous — instead of waiting for the connection to complete, it provides a callback that runs when the result is ready.
if (err) — If err exists (is not null), the connection failed. The error object contains details about what went wrong.
console.log(...) — Prints messages to the terminal (visible in the EC2 SSH session or PM2 logs). These messages help you know if the database is connected or not when the server starts.
Line 32:
app.use(express.json());app.use(...) — Registers middleware. Middleware is a function that processes every incoming request BEFORE it reaches a route handler.
express.json() — Built-in middleware that parses JSON request bodies. When the browser sends POST /api/expenses with a JSON body like {"description":"Groceries","amount":1200}, this middleware:
- Reads the raw request body (a string of text)
- Parses it as JSON
- Makes it available as
req.body(a JavaScript object)
Without this middleware, req.body would be undefined and you couldn't read the form data.
Line 33:
app.use(express.static(path.join(__dirname, "public")));express.static(directory) — Built-in middleware that serves static files (HTML, CSS, JS, images) from a folder.
path.join(__dirname, "public") — Constructs the absolute path to the public folder:
__dirname— A Node.js global variable that contains the absolute path of the directory whereserver.jsis located (e.g.,/home/ubuntu/expense-tracker)path.join(...)— Joins path segments with the correct separator
Result: /home/ubuntu/expense-tracker/public
What this does: When a browser requests http://13.201.76.0:3000/style.css, Express automatically looks for public/style.css and serves it. Same for script.js, index.html, etc. This is why the browser can load the HTML page, CSS styles, and JavaScript without any explicit route for each file.
Lines 38-40: Home Page Route
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "public", "index.html"));
});app.get("/", handler) — Registers a route handler for GET / (the root URL).
"/" — The URL path. When someone visits http://13.201.76.0:3000/, the path is /.
(req, res) => { ... } — The handler function:
req— Request object — contains info about the incoming request (headers, URL, query params, body)res— Response object — used to send data back to the browser
res.sendFile(...) — Sends a file as the response. The browser receives the HTML file and renders it.
Lines 43-53: GET all expenses
app.get("/api/expenses", async (req, res) => {
try {
const result = await pool.query(
"SELECT * FROM expenses ORDER BY date DESC"
);
res.json(result.rows);
} catch (err) {
console.log("Error:", err);
res.status(500).json({ error: "Failed to fetch expenses" });
}
});app.get("/api/expenses", handler) — Handles GET /api/expenses. The frontend calls this to load all expenses.
async (req, res) => { ... } — async keyword enables await inside the function. Database queries take time; await pauses execution until the query completes.
try { ... } catch (err) { ... } — Error handling. If anything inside try throws an error, execution jumps to catch instead of crashing the server.
Inside the try block:
pool.query("SELECT * FROM expenses ORDER BY date DESC")— Sends a SQL query to PostgreSQLawait— Waits for PostgreSQL to execute the query and return resultsresult— The query result object, which containsresult.rows(an array of expense objects)res.json(result.rows)— Sends the rows array as a JSON response to the browser. Express automatically converts the JavaScript array to a JSON string, sets theContent-Typeheader toapplication/json, and sends the response.
Lines 56-74: POST Add New Expense
app.post("/api/expenses", async (req, res) => {
const { description, amount, category, date } = req.body;
// Check if all fields are filled
if (!description || !amount || !category || !date) {
return res.status(400).json({ error: "Please fill all fields" });
}
try {
const result = await pool.query(
"INSERT INTO expenses (description, amount, category, date) VALUES ($1, $2, $3, $4) RETURNING *",
[description, amount, category, date]
);
res.json(result.rows[0]);
} catch (err) {
console.log("Error:", err);
res.status(500).json({ error: "Failed to add expense" });
}
});app.post("/api/expenses", handler) — Handles POST /api/expenses. The frontend calls this when the user submits the "Add Expense" form.
Destructuring the request body: When the browser sends POST /api/expenses with a JSON body like {"description":"Groceries","amount":1200,"category":"Food","date":"2025-11-01"}, this line extracts each value:
const { description, amount, category, date } = req.body;Result:
description="Groceries"amount=1200category="Food"date="2025-11-01"
Validation:
if (!description || !amount || !category || !date) {
return res.status(400).json({ error: "Please fill all fields" });
}!description — The ! (NOT) operator. If description is undefined, null, "" (empty), or 0, then !description is true.
|| — OR operator. If ANY of the four fields is missing, the condition is true.
return res.status(400).json(...) — Sends a 400 (Bad Request) error and stops the function (the return keyword exits the function immediately so the code below doesn't run).
This is server-side validation — even if someone bypasses the browser form, the server still checks the data. Never trust client-side validation alone.
Database Insert:
const result = await pool.query(
"INSERT INTO expenses (description, amount, category, date) VALUES ($1, $2, $3, $4) RETURNING *",
[description, amount, category, date]
);INSERT INTO expenses (description, amount, category, date)— SQL command to add a new row to theexpensestable, specifying which columns to fillVALUES ($1, $2, $3, $4)— Parameterized query (prepared statement). The$1, $2, $3, $4are placeholders that get replaced with actual values[description, amount, category, date]— The second argument is an array of values that map to the placeholders:$1→description,$2→amount,$3→category,$4→date
Why parameterized queries? Security! Prevents SQL injection (see Q12 above).
RETURNING *— PostgreSQL-specific feature. After inserting the row, it returns the complete newly-created row (including the auto-generatedidandcreated_at)result.rows[0]— The first (and only) row returned byRETURNING *— the newly created expenseres.json(result.rows[0])— Sends this back to the browser as JSON. The browser can then use this to confirm the expense was added.
Lines 77-95: DELETE Single Expense
app.delete("/api/expenses/:id", async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query(
"DELETE FROM expenses WHERE id = $1 RETURNING *",
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Expense not found" });
}
res.json({ message: "Expense deleted successfully" });
} catch (err) {
console.log("Error:", err);
res.status(500).json({ error: "Failed to delete expense" });
}
});app.delete("/api/expenses/:id", handler) — Handles DELETE /api/expenses/5 (or any ID).
":id" — A route parameter. The colon makes it a variable. When a request comes for /api/expenses/5, Express captures 5 and stores it in req.params.id.
const { id } = req.params; — Destructures the route parameter. id = "5".
"DELETE FROM expenses WHERE id = $1 RETURNING *" — SQL command:
DELETE FROM expenses— Delete a row from the expenses tableWHERE id = $1— Only the row whereidmatches the parameterRETURNING *— Return the deleted row (so we can check if anything was actually deleted)
if (result.rows.length === 0) — If RETURNING * returned no rows, it means no row with that ID existed. We return a 404 (Not Found).
res.json({ message: "Expense deleted successfully" }) — If deletion worked, send a success message.
Lines 98-106: DELETE ALL Expenses
app.delete("/api/expenses", async (req, res) => {
try {
await pool.query("DELETE FROM expenses");
res.json({ message: "All expenses deleted successfully" });
} catch (err) {
console.log("Error:", err);
res.status(500).json({ error: "Failed to delete all expenses" });
}
});app.delete("/api/expenses", handler) — Handles DELETE /api/expenses (no ID = delete ALL).
"DELETE FROM expenses" — SQL to delete every row in the table. No WHERE clause = all rows.
No RETURNING * needed because we don't care about the deleted data.
Lines 109-113: Start the Server
app.listen(PORT, () => {
console.log("=================================");
console.log(`🚀 Server running on http://localhost:${PORT}`);
console.log("=================================");
});app.listen(PORT, callback) — Tells Express to start the HTTP server and listen for connections on port 3000.
The callback runs once the server is ready — it logs a confirmation message.
After this line, the server is running. It will wait indefinitely for HTTP requests and respond to them. PM2 keeps this process alive on EC2.
-- Step 1: Create database
CREATE DATABASE expense_tracker;
-- Step 2: Connect to database (in psql, run: \c expense_tracker)
-- Step 3: Create expenses table
CREATE TABLE expenses (
id SERIAL PRIMARY KEY,
description VARCHAR(255) NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
category VARCHAR(100) NOT NULL,
date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Step 4: Add some sample data (optional)
INSERT INTO expenses (description, amount, category, date) VALUES
('Grocery shopping', 1200.00, 'Food', '2025-11-01'),
('Auto rickshaw', 80.00, 'Transportation', '2025-11-02'),
('Movie tickets', 500.00, 'Entertainment', '2025-11-05'),
('Electricity bill', 2500.00, 'Bills', '2025-11-06'),
('Chai and samosa', 50.00, 'Food', '2025-11-07');-- is a SQL comment. Everything after on that line is ignored.
CREATE DATABASE — SQL command to create a new database.
expense_tracker — The name of the database. This matches database: "expense_tracker" in server.js.
; — SQL statements end with a semicolon.
A database is a container that holds tables. One PostgreSQL server can have many databases.
CREATE TABLE expenses — Creates a new table named expenses inside the current database.
A table is like a spreadsheet with defined columns. Each row is one expense record.
| Column | Type | Purpose |
|---|---|---|
id SERIAL PRIMARY KEY |
Auto-incrementing integer | Unique identifier for each expense. PRIMARY KEY = no two rows can have the same id. |
description VARCHAR(255) NOT NULL |
Variable-length text up to 255 characters | What the expense was for. NOT NULL = required. |
amount DECIMAL(10, 2) NOT NULL |
Number with 10 total digits, 2 decimal places | Expense amount in ₹. Perfect for money. NOT NULL = required. |
category VARCHAR(100) NOT NULL |
Text up to 100 characters | Expense category (Food, Bills, etc.). NOT NULL = required. |
date DATE NOT NULL |
Date (year-month-day format) | When the expense occurred. NOT NULL = required. |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
Date + time | When the record was created. Auto-set by PostgreSQL. |
INSERT INTO expenses (description, amount, category, date) — Insert new rows, specifying which columns to fill.
VALUES (...) — The actual data for each row.
Multiple rows are separated by commas. These are sample/test data — they populate the table so the app has something to display immediately after setup. This step is optional.
{
"name": "expense-tracker",
"version": "1.0.0",
"description": "Simple expense tracker for beginners",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"keywords": [
"expense",
"tracker"
],
"author": "ObaidAbdullah16",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.0"
}
}| Field | Value | Purpose |
|---|---|---|
"name" |
"expense-tracker" |
Project identifier. Must be lowercase, no spaces. |
"version" |
"1.0.0" |
Semantic versioning: MAJOR.MINOR.PATCH. First stable release. |
"description" |
"Simple expense tracker for beginners" |
Brief text about the project. |
"main" |
"server.js" |
The entry point file. When the project is loaded, Node.js starts here. |
"scripts"."start" |
"node server.js" |
Running npm start in the terminal executes node server.js. This is the command that starts the server. |
"keywords" |
["expense", "tracker"] |
Search terms if published to npm. |
"author" |
"ObaidAbdullah16" |
Who created the project. |
"license" |
"ISC" |
ISC license — a permissive open-source license similar to MIT. |
"dependencies" |
{express, pg} |
Packages this project needs to run. When you run npm install, npm reads this and downloads them. |
"express": "^4.18.2"— The^(caret) means: install version 4.18.2 or any compatible higher version (4.18.3, 4.19.0) but NOT 5.0.0 (major version change)"pg": "^8.11.0"— Install 8.11.0 or compatible higher (8.11.1, 8.12.0) but NOT 9.0.0
Auto-generated by npm when you run npm install. Never edit manually.
While package.json says "I need Express ~4.18.2", package-lock.json says "I need Express exactly 4.18.2, AND its sub-dependency accepts at exactly 1.3.8, AND accepts's sub-dependency mime-types at exactly 2.1.35..."
| Without lock file | With lock file |
|---|---|
npm install might install different sub-versions on different machines |
npm install installs identical versions everywhere |
| App might behave differently on EC2 vs your laptop | Guaranteed same behavior |
| Debugging version-related bugs is hard | Reproducible builds |
# Database Configuration
DB_USER=postgres
DB_HOST=localhost
DB_NAME=expense_tracker
DB_PASSWORD=yourpassword
DB_PORT=5432
# Server Configuration
PORT=3000
NODE_ENV=development
# For production, you might want to add:
# DB_HOST=your-rds-endpoint.amazonaws.com (if using AWS RDS)
A template file showing what environment variables the project needs. It does NOT contain real values — it shows the structure so anyone setting up the project knows what to configure.
- Copy
.env.exampleto.env:cp .env.example .env - Edit
.envwith your real values:DB_PASSWORD=MyRealPassword123 - The
.envfile is listed in.gitignoreso it never gets committed - The server reads from
.envusing a library likedotenv
| Problem | Solution |
|---|---|
| Passwords in code get committed to GitHub (public!) | Store secrets in .env (never committed) |
| Different environments need different config | Each environment has its own .env |
| New developers don't know what config is needed | .env.example documents all required variables |
Currently, server.js hardcodes the password directly instead of reading from .env. The .env.example exists as documentation for the best-practice approach.
# Ignore node_modules folder
node_modules/
# Ignore environment files (if you create .env later)
.env
# Ignore logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Ignore OS files
.DS_Store
Thumbs.db
# Ignore editor files
.vscode/
.idea/
*.swp
*.swo
*~
| Entry | What It Excludes | Why |
|---|---|---|
node_modules/ |
All npm packages (thousands of files, ~50MB+) | Recreated by npm install. Bloats the repo. |
.env |
Environment variables file with real secrets | Contains passwords. MUST never be in a public repo. |
npm-debug.log* |
npm error logs | Generated files, not source code. The * is a wildcard matching any suffix. |
yarn-debug.log* |
Yarn package manager logs | Same as above, for Yarn users. |
yarn-error.log* |
Yarn error logs | Same. |
.DS_Store |
macOS hidden folder metadata file | Auto-generated by Finder. Not project-related. |
Thumbs.db |
Windows thumbnail cache | Auto-generated by Windows Explorer. Not project-related. |
.vscode/ |
VS Code editor settings | Personal to each developer — shouldn't be shared. |
.idea/ |
JetBrains IDE settings (WebStorm, IntelliJ) | Same — personal editor preferences. |
*.swp, *.swo |
Vim editor swap files | Temporary files created while editing in Vim. |
*~ |
Backup files created by some editors | Emacs and others create filename~ backups. |
This is what the user sees. The browser loads this file and renders it as a visual page. (See full file in repository for complete HTML structure.)
Head Section:
- Meta tags for character encoding and responsiveness
- Bootstrap CSS link from CDN
- Custom CSS link (style.css)
Body Section:
- Title: "💰 Expense Tracker"
- Add Expense Form with inputs for:
- Description (text)
- Amount (number with step="0.01")
- Category (select dropdown)
- Date (date picker)
- Total Expenses Display (updated dynamically)
- Expenses Table with:
- Thead: Column headers (Date, Description, Category, Amount, Action)
- Tbody: Dynamically populated by script.js
Scripts:
- Bootstrap JS (at bottom)
- script.js (at bottom)
Why scripts at bottom? By the time JavaScript runs, all HTML has been parsed and rendered. The JavaScript can safely access all elements by their IDs.
This runs in the browser (not on the server). It's the bridge between the visible HTML page and the server API. (See full file in repository for complete JavaScript code.)
loadExpenses() — Fetches all expenses from /api/expenses and rebuilds the table
- Calls GET /api/expenses
- Handles empty state
- Loops through expenses, creates table rows
- Applies category color coding
- Calculates and displays total with Indian number formatting
Form Submit Handler — When user submits the form
- Prevents default form behavior
- Collects form values
- Sends POST request to
/api/expenses - On success: resets form, reloads table, shows alert
- On error: shows error alert
deleteExpense(id, description) — Delete a single expense
- Shows confirmation dialog
- Sends DELETE request to
/api/expenses/:id - On success: reloads table, shows alert
Delete All Handler — Delete all expenses
- Shows confirmation dialog ("
⚠️ Delete ALL expenses?") - Sends DELETE request to
/api/expenses - On success: reloads table, shows alert
Contains custom CSS that makes the app look polished. (See full file in repository for complete styles.)
- Body: Purple gradient background (linear-gradient 135deg from #667eea to #764ba2)
- H1: White text with text-shadow
- Cards: Rounded corners (15px), box-shadow, no border
- Buttons: Primary (purple), hover effect (darker purple)
- Table: Dark header with matching purple
- Badges: Color-coded by category (green, red, blue, etc.)
The quick-start guide visible on the GitHub repository page. Contains project description, features, tech stack, setup instructions, deployment guide, and troubleshooting.