Web-based Habit Streak Tracker JavaScript, PHP, MySQL
👤 Sharing: AI
Okay, here's a basic habit streak tracker implementation using JavaScript (front-end), PHP (back-end), and MySQL (database). I'll provide the code and explanations. This is a simplified example to illustrate the core concepts.
**Important Considerations:**
* **Security:** This example doesn't include robust security measures like input sanitization, prepared statements, or authentication. In a real-world application, you *must* implement these to prevent vulnerabilities.
* **Error Handling:** Error handling is minimal for brevity. In a production environment, you'd want comprehensive error logging and user-friendly error messages.
* **Front-End Framework:** This uses vanilla JavaScript. For more complex applications, consider using a framework like React, Vue, or Angular.
* **CSS:** I haven't included CSS for styling to keep the example concise. You'll need to add your own CSS to make it look presentable.
**1. Database Setup (MySQL)**
```sql
-- Create the database
CREATE DATABASE habit_tracker;
-- Use the database
USE habit_tracker;
-- Create the habits table
CREATE TABLE habits (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create the streaks table
CREATE TABLE streaks (
id INT AUTO_INCREMENT PRIMARY KEY,
habit_id INT NOT NULL,
date DATE NOT NULL,
completed BOOLEAN DEFAULT FALSE,
FOREIGN KEY (habit_id) REFERENCES habits(id)
);
```
**Explanation:**
* `habits` table: Stores the names of the habits.
* `id`: Unique identifier for each habit (auto-incrementing integer).
* `name`: The name of the habit (e.g., "Exercise", "Read").
* `created_at`: Timestamp indicating when the habit was created.
* `streaks` table: Tracks the completion status of habits on specific dates.
* `id`: Unique identifier for each streak record.
* `habit_id`: Foreign key referencing the `habits` table, indicating which habit this streak belongs to.
* `date`: The date for which the streak is recorded.
* `completed`: Boolean value (0 or 1) indicating whether the habit was completed on that date.
**2. PHP Backend (API Endpoints)**
Create a directory on your web server (e.g., `habit_tracker`). Place the following PHP files within that directory:
**`config.php`:**
```php
<?php
// Database configuration
$host = "localhost";
$username = "your_db_username";
$password = "your_db_password";
$database = "habit_tracker";
// Create connection
$conn = new mysqli($host, $username, $password, $database);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
header('Content-Type: application/json'); // Set response type to JSON
header("Access-Control-Allow-Origin: *"); // Allow requests from any origin (CORS) - USE WITH CAUTION in production!
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
?>
```
**Explanation:**
* This file contains the database connection information. **Replace the placeholder values** with your actual MySQL credentials.
* It also sets the `Content-Type` header to `application/json`, indicating that the API will return JSON responses.
* CORS headers are added to allow cross-origin requests from the JavaScript front-end (running in the browser). **Important:** In a production environment, you should restrict `Access-Control-Allow-Origin` to only the domain(s) where your front-end is hosted.
**`habits.php`:** (Handles CRUD operations for habits)
```php
<?php
require_once 'config.php';
$method = $_SERVER['REQUEST_METHOD'];
switch ($method) {
case 'GET':
// Get all habits
$sql = "SELECT * FROM habits";
$result = $conn->query($sql);
$habits = array();
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$habits[] = $row;
}
}
echo json_encode($habits);
break;
case 'POST':
// Create a new habit
$data = json_decode(file_get_contents('php://input'), true);
$name = $data['name'];
$sql = "INSERT INTO habits (name) VALUES ('$name')";
if ($conn->query($sql) === TRUE) {
$new_habit_id = $conn->insert_id;
echo json_encode(['message' => 'Habit created successfully', 'id' => $new_habit_id]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Error creating habit: ' . $conn->error]);
}
break;
case 'PUT':
// Update a habit
$data = json_decode(file_get_contents('php://input'), true);
$id = $data['id'];
$name = $data['name'];
$sql = "UPDATE habits SET name='$name' WHERE id=$id";
if ($conn->query($sql) === TRUE) {
echo json_encode(['message' => 'Habit updated successfully']);
} else {
http_response_code(500);
echo json_encode(['error' => 'Error updating habit: ' . $conn->error]);
}
break;
case 'DELETE':
// Delete a habit
$id = $_GET['id']; // Get ID from query parameter
$sql = "DELETE FROM habits WHERE id=$id";
if ($conn->query($sql) === TRUE) {
echo json_encode(['message' => 'Habit deleted successfully']);
} else {
http_response_code(500);
echo json_encode(['error' => 'Error deleting habit: ' . $conn->error]);
}
break;
default:
http_response_code(405); // Method Not Allowed
echo json_encode(['error' => 'Method not allowed']);
}
$conn->close();
?>
```
**Explanation:**
* This script handles HTTP requests for the `habits` resource.
* It uses a `switch` statement to determine the appropriate action based on the HTTP method (GET, POST, PUT, DELETE).
* **GET:** Retrieves all habits from the database and returns them as a JSON array.
* **POST:** Creates a new habit in the database. It reads the habit data (name) from the request body (which should be JSON).
* **PUT:** Updates an existing habit. It reads the habit ID and new name from the request body.
* **DELETE:** Deletes a habit from the database. It reads the habit ID from the query string (e.g., `/habits.php?id=123`).
* Error handling: Sets the HTTP response code to 500 (Internal Server Error) for database errors.
* `file_get_contents('php://input')`: Reads the raw data from the request body. This is used to get the JSON data sent with POST and PUT requests.
* `json_decode()`: Converts the JSON data from the request body into a PHP associative array.
* `json_encode()`: Converts a PHP array into a JSON string, which is sent as the response.
* `http_response_code()`: Sets the HTTP response code (e.g., 200 for success, 405 for method not allowed, 500 for error).
* **Important:** This uses string interpolation in the SQL queries (e.g., `WHERE id=$id`). This is a security risk (SQL injection). **Always use prepared statements** to prevent SQL injection in a production environment.
**`streaks.php`:** (Handles retrieving and updating streaks)
```php
<?php
require_once 'config.php';
$method = $_SERVER['REQUEST_METHOD'];
switch ($method) {
case 'GET':
// Get streaks for a specific habit and date range
$habit_id = $_GET['habit_id'];
$start_date = $_GET['start_date'];
$end_date = $_GET['end_date'];
$sql = "SELECT * FROM streaks WHERE habit_id = $habit_id AND date >= '$start_date' AND date <= '$end_date'";
$result = $conn->query($sql);
$streaks = array();
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$streaks[] = $row;
}
}
echo json_encode($streaks);
break;
case 'POST':
// Update the completion status for a streak
$data = json_decode(file_get_contents('php://input'), true);
$habit_id = $data['habit_id'];
$date = $data['date'];
$completed = $data['completed'] ? 1 : 0; // Convert boolean to 1/0
// Check if streak already exists
$sql = "SELECT id FROM streaks WHERE habit_id = $habit_id AND date = '$date'";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
// Update existing streak
$sql = "UPDATE streaks SET completed = $completed WHERE habit_id = $habit_id AND date = '$date'";
} else {
// Create new streak
$sql = "INSERT INTO streaks (habit_id, date, completed) VALUES ($habit_id, '$date', $completed)";
}
if ($conn->query($sql) === TRUE) {
echo json_encode(['message' => 'Streak updated successfully']);
} else {
http_response_code(500);
echo json_encode(['error' => 'Error updating streak: ' . $conn->error]);
}
break;
default:
http_response_code(405); // Method Not Allowed
echo json_encode(['error' => 'Method not allowed']);
}
$conn->close();
?>
```
**Explanation:**
* This script handles retrieving streak data and updating the completion status of streaks.
* **GET:** Retrieves streaks for a specific habit within a given date range. The `habit_id`, `start_date`, and `end_date` are passed as query parameters.
* **POST:** Updates the completion status of a streak for a given habit and date. If a streak doesn't already exist for that habit and date, it creates a new streak record.
* Error handling: Similar to `habits.php`.
* Boolean to 1/0 conversion: Converts the JavaScript boolean `completed` value to 1 or 0 for storage in the MySQL database.
* **Important:** This also uses string interpolation in the SQL queries. **Always use prepared statements** to prevent SQL injection.
**3. JavaScript Front-End (HTML and JavaScript)**
Create an HTML file (e.g., `index.html`) and a JavaScript file (e.g., `script.js`) in the same directory as the PHP files (or in a subdirectory if you prefer).
**`index.html`:**
```html
<!DOCTYPE html>
<html>
<head>
<title>Habit Tracker</title>
<style>
/* Basic styling - replace with your own CSS */
body { font-family: sans-serif; }
.habit-container { margin-bottom: 20px; }
.streak-calendar { display: grid; grid-template-columns: repeat(7, 30px); gap: 5px; }
.streak-day { width: 30px; height: 30px; border: 1px solid #ccc; text-align: center; }
.streak-completed { background-color: lightgreen; }
</style>
</head>
<body>
<h1>Habit Tracker</h1>
<!-- Add Habit Form -->
<div>
<h2>Add New Habit</h2>
<input type="text" id="new-habit-name" placeholder="Habit Name">
<button onclick="addHabit()">Add Habit</button>
</div>
<!-- Habit List -->
<div id="habits-container">
</div>
<script src="script.js"></script>
</body>
</html>
```
**Explanation:**
* Basic HTML structure with a title, a form for adding new habits, and a container to display the habits.
* Includes a placeholder for basic styling. You'll want to replace this with your own CSS.
* Includes the `script.js` file.
**`script.js`:**
```javascript
const apiBaseUrl = ''; // Replace with your API base URL (e.g., http://localhost/habit_tracker)
document.addEventListener('DOMContentLoaded', () => {
loadHabits();
});
async function loadHabits() {
const response = await fetch(`${apiBaseUrl}/habits.php`);
const habits = await response.json();
const habitsContainer = document.getElementById('habits-container');
habitsContainer.innerHTML = ''; // Clear existing habits
habits.forEach(habit => {
const habitDiv = document.createElement('div');
habitDiv.classList.add('habit-container');
habitDiv.innerHTML = `<h2>${habit.name}</h2>`;
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30); // Display last 30 days
const endDate = new Date();
const calendarDiv = document.createElement('div');
calendarDiv.classList.add('streak-calendar');
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
const dayDiv = document.createElement('div');
dayDiv.classList.add('streak-day');
dayDiv.textContent = currentDate.getDate();
dayDiv.dataset.date = currentDate.toISOString().slice(0, 10); // YYYY-MM-DD
dayDiv.addEventListener('click', () => toggleStreak(habit.id, dayDiv.dataset.date, dayDiv));
calendarDiv.appendChild(dayDiv);
currentDate.setDate(currentDate.getDate() + 1);
}
habitDiv.appendChild(calendarDiv);
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete Habit';
deleteButton.addEventListener('click', () => deleteHabit(habit.id));
habitDiv.appendChild(deleteButton);
habitsContainer.appendChild(habitDiv);
loadStreaks(habit.id, startDate.toISOString().slice(0, 10), endDate.toISOString().slice(0, 10), calendarDiv);
});
}
async function loadStreaks(habitId, startDate, endDate, calendarDiv) {
const response = await fetch(`${apiBaseUrl}/streaks.php?habit_id=${habitId}&start_date=${startDate}&end_date=${endDate}`);
const streaks = await response.json();
streaks.forEach(streak => {
const dayDiv = calendarDiv.querySelector(`[data-date="${streak.date}"]`);
if (dayDiv) {
if (streak.completed) {
dayDiv.classList.add('streak-completed');
}
}
});
}
async function toggleStreak(habitId, date, dayDiv) {
const completed = !dayDiv.classList.contains('streak-completed'); // Toggle completion
const response = await fetch(`${apiBaseUrl}/streaks.php`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
habit_id: habitId,
date: date,
completed: completed
})
});
if (response.ok) {
if (completed) {
dayDiv.classList.add('streak-completed');
} else {
dayDiv.classList.remove('streak-completed');
}
} else {
alert('Failed to update streak');
}
}
async function addHabit() {
const name = document.getElementById('new-habit-name').value;
if (!name) return;
const response = await fetch(`${apiBaseUrl}/habits.php`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: name })
});
if (response.ok) {
document.getElementById('new-habit-name').value = '';
loadHabits(); // Reload habits after adding
} else {
alert('Failed to add habit');
}
}
async function deleteHabit(id) {
if (confirm('Are you sure you want to delete this habit?')) {
const response = await fetch(`${apiBaseUrl}/habits.php?id=${id}`, {
method: 'DELETE'
});
if (response.ok) {
loadHabits(); // Reload habits after deleting
} else {
alert('Failed to delete habit');
}
}
}
```
**Explanation:**
* **`apiBaseUrl`**: **Replace this** with the correct URL to your PHP API endpoint (e.g., `"http://localhost/habit_tracker"`). This is essential.
* **`loadHabits()`**: Fetches all habits from the API and displays them on the page.
* It iterates through the habits and creates a `div` for each one.
* For each habit, it generates a calendar of the last 30 days.
* Each day in the calendar is a clickable `div` that calls `toggleStreak()` when clicked.
* It adds a "Delete Habit" button that calls `deleteHabit()` when clicked.
* After displaying the habit, it calls `loadStreaks()` to load the streak data for that habit.
* **`loadStreaks()`**: Fetches the streaks for a specific habit and date range from the API and highlights the completed days in the calendar.
* **`toggleStreak()`**: Toggles the completion status of a streak for a specific habit and date.
* It sends a POST request to the `streaks.php` endpoint to update the streak in the database.
* It updates the visual appearance of the calendar day based on the completion status.
* **`addHabit()`**: Adds a new habit to the database.
* It sends a POST request to the `habits.php` endpoint.
* It reloads the habits after adding a new one.
* **`deleteHabit()`**: Deletes a habit from the database.
* It sends a DELETE request to the `habits.php` endpoint.
* It reloads the habits after deleting one.
* **DOM manipulation**: Uses `document.createElement`, `appendChild`, `innerHTML`, etc., to dynamically create and update the HTML elements.
* **`async/await`**: Uses `async` and `await` to make asynchronous API calls. This makes the code easier to read and write.
* **Error handling**: Basic error handling with `alert()` for API failures.
**How to Run:**
1. **Set up your web server:** You'll need a web server (like Apache or Nginx) with PHP and MySQL installed.
2. **Configure MySQL:** Create the `habit_tracker` database and the `habits` and `streaks` tables using the SQL code provided. Also, update the `config.php` file with your MySQL credentials.
3. **Place the files:** Place the PHP files (`config.php`, `habits.php`, `streaks.php`) and the HTML and JavaScript files (`index.html`, `script.js`) in the document root of your web server (or in a subdirectory).
4. **Update `apiBaseUrl`:** Edit the `script.js` file and replace the placeholder value for `apiBaseUrl` with the correct URL to your PHP API endpoint (e.g., `"http://localhost/habit_tracker"`).
5. **Open in browser:** Open `index.html` in your web browser.
**Important Notes/Next Steps:**
* **Security:** As mentioned before, this example is missing critical security measures. Implement input validation, sanitization, and, most importantly, **use prepared statements** in your PHP code to prevent SQL injection. Also, implement proper authentication and authorization to protect your data.
* **User Interface:** The front-end is very basic. You'll want to improve the user interface using CSS and possibly a front-end framework.
* **Date Handling:** Consider using a JavaScript date library like Moment.js or date-fns for more robust date handling.
* **Error Handling:** Implement more comprehensive error handling on both the front-end and back-end.
* **Code Organization:** As your application grows, you'll want to refactor the code into smaller, more manageable modules.
* **Testing:** Write unit tests and integration tests to ensure the reliability of your code.
This example provides a basic foundation for building a habit streak tracker. Remember to prioritize security and improve the user experience as you develop your application further. Good luck!
👁️ Viewed: 9
Comments