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