Rabikant

Posted on March 9th

How to Build a Live Leaderboard App with PHP, HTML, and WebSockets

"let's Learn how to Build a Live Leaderboard App with PHP, HTML, and WebSockets"

In this blog, we will walk through the creation of a real-time live leaderboard using JavaScript, PHP, and PieSocket for WebSocket-based real-time updates. This project will allow players to update their scores, which will automatically reflect on the leaderboard without refreshing the page.

Features of the Live Leaderboard

  • Displays a ranking table with players and their scores.
  • Updates scores dynamically using a simple form.
  • Uses WebSockets (PieSocket) for real-time score updates.
  • Backend integration with PHP for handling score updates.

Basic HTML Structure & External Libraries

At the beginning of the file, we include essential metadata and external libraries.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Live Leaderboard</title>
    <script src="<https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js>"></script>
    <script src="<https://unpkg.com/piesocket-js@5>"></script>
</head>
  • meta charset="UTF-8" → Ensures proper text encoding.
  • meta name="viewport" → Makes the page responsive on different devices.
  • Axios Library → Used for making HTTP requests.
  • PieSocket Library → Enables real-time WebSocket communication.

CSS Styling for Leaderboard

The embedded CSS styles the page with a dark theme and a clean leaderboard layout.

<style>
    body {
        font-family: 'Arial', sans-serif;
        background-color: #121212;
        color: #fff;
        text-align: center;
        padding: 20px;
    }

    h2 {
        font-size: 2rem;
    }

    table {
        width: 60%;
        margin: auto;
        border-collapse: collapse;
        background: #1e1e1e;
        border-radius: 10px;
        overflow: hidden;
    }

    th {
        background: #333;
        color: #ffcc00;
        padding: 15px;
    }

    td {
        background: #222;
        padding: 15px;
    }
</style>
  • The background is dark (#121212).
  • The table has a rounded border, alternating row colors, and bold headers.
  • The gold color (#ffcc00) highlights important elements.

Leaderboard Table Structure

The table is structured with headers and a body where player scores will be dynamically injected.


<body>
    <h2>Live Leaderboard</h2>
    <table>
        <thead>
            <tr>
                <th>Rank</th>
                <th>Player</th>
                <th>Score</th>
            </tr>
        </thead>
        <tbody id="leaderboard"></tbody>
    </table>

Score Update Form

A simple form allows users to update scores.

<h3>Update Score</h3>
<div id="scoreDiv">
    <label for="player">Player Name:</label>
    <select id="player">
        <option value="Player 1">Player 1</option>
        <option value="Player 2">Player 2</option>
        <option value="Player 3">Player 3</option>
    </select>
    <label for="score">New Score:</label>
    <input type="number" id="score" required>
    <button type="button" id="updateButton">Update Scores</button>
</div>
  • The user selects a player from a dropdown. Inputs a new score .
  • Clicks a button to submit the update.

JavaScript for Fetching and Updating Scores

Setting Up Event Listeners and Variables

document.addEventListener("DOMContentLoaded", function () {
    let leaderboardTable = document.getElementById("leaderboard");
    let playerNameInput = document.getElementById("player");
    let playerScoreInput = document.getElementById("score");
    let updateButton = document.getElementById("updateButton");

    let scores = {};
  • Purpose: Ensures the script runs only after the HTML document is fully loaded.
  • Variables:
    • leaderboardTable: The section of the leaderboard where player scores will be displayed.
    • playerNameInput: A  dropdown where the user selects a player. playerScoreInput: An  field where the user enters a new score.
    • updateButton: A button that submits the updated score.
    • scores: An object to store player scores locally.

Handling Score Updates

updateButton.addEventListener("click", function (e) {
        e.preventDefault();
        console.log("Button clicked");

        let playerName = playerNameInput.value.trim();
        let playerScore = parseInt(playerScoreInput.value);

        if (playerName === "" || isNaN(playerScore)) {
            alert("Enter a valid player name and score!");
            return;
        }

        // Update the local scores object.
        scores[playerName] = playerScore;

        // Make an Axios POST request to update the backend.
        axios.post("<http://127.0.0.1:8000/update_score.php>", {
            player: { name: playerName, score: playerScore }
        })
            .then(response => {
                console.log("Score updated successfully:", response.data);
                updateLeaderboard(); // Refresh leaderboard after updating scores
            })
            .catch(error => {
                console.error("Error updating score:", error);
            });

        playerScoreInput.value = "";
    });
  • Event Listener: When the update button is clicked:
    • Prevents the default form submission behavior.
    • Logs the button click event.
    • Extracts player name and score.
    • Validates inputs (prevents empty names or non-numeric scores).
    • Updates the scores object locally.
    • Sends the new score to the backend using Axios (HTTP POST request).
    • If the request is successful, it calls updateLeaderboard() to refresh the leaderboard.

Setting Up Real-Time Updates with PieSocket

var piesocket = new PieSocket.default({
        clusterId: "CLUSTER_ID",
        apiKey: "API_KEY",
        notifySelf: true,
        presence: true
    });

    piesocket.subscribe("leaderboard-channel").then(ch => {
        channel = ch;
        channel.listen("scoreUpdate", function (data) {
            console.log("Received event from PieSocket:", data);
            if (data && data.name && data.score !== undefined) {
                scores[data.name] = data.score;
                updateLeaderboard();
            }
        });
    });
  • PieSocket Initialization: Connects to the WebSocket service to listen for real-time leaderboard updates.
  • Subscribing to a Channel: Joins leaderboard-channel to receive live updates.
  • Listening for Score Updates:
    • When a scoreUpdate event is received, it updates the scores object.
    • Calls updateLeaderboard() to reflect changes in the UI.

Fetching Leaderboard Data

   function updateLeaderboard() {
        // Fetch the leaderboard data from get_score.php
        axios.get("<http://127.0.0.1:8000/get_score.php>")
            .then(response => {
                if (response.data.status === "success") {
                    const scores = response.data.data;

                    // Update the table with retrieved data
                    leaderboardTable.innerHTML = "";
                    scores.forEach((player, index) => {
                        let row = document.createElement("tr");
                        row.innerHTML = `<td>${index + 1}</td><td>${player.player_name}</td><td>${player.score}</td>`;
                        leaderboardTable.appendChild(row);
                    });

                    console.log("Leaderboard updated with data from the database!");
                } else {
                    console.error("Error fetching leaderboard data:", response.data);
                }
            })
            .catch(error => {
                console.error("Error fetching leaderboard data:", error);
            });
    }
  • Purpose: Fetches the leaderboard data from get_score.php using an Axios GET request.
  • If successful:
    • Extracts player scores from the response.
    • Clears the leaderboard table.
    • Dynamically populates rows with player names and scores.
    • Logs the update.
  • If failed:
    • Logs an error message.

Initial Leaderboard Fetch

updateLeaderboard();
});
  • Calls updateLeaderboard() once when the page loads to display the current scores.

Setting HTTP Headers for CORS

header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Content-Type: application/json");
  • These headers allow cross-origin requests from any domain ().
  • It enables only POST and OPTIONS methods.
  • Content-Type: application/json ensures that requests and responses use JSON.

Handling Preflight Requests

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit();
}
  • Browsers send OPTIONS requests before POST requests (CORS preflight).
  • If it's an OPTIONS request, the script exits immediately with 200 OK.

Reading and Validating Incoming JSON Data

$data = json_decode(file_get_contents('php://input'), true);

if (!isset($data['player']['name']) || !isset($data['player']['score'])) {
    http_response_code(400);
    echo json_encode(["error" => "Invalid request"]);
    exit();
}
  • Reads raw JSON input and converts it into a PHP array ($data).
  • Validates that the required fields (name and score) exist.
  • If missing, returns 400 Bad Request and exits.

Extracting and Sanitizing Input

$playerName = $data['player']['name'];
$playerScore = intval($data['player']['score']);
  • Extracts the player's name and score from the request.
  • intval() ensures the score is always an integer (prevents injection attacks).

Connecting to the Database

$conn = new mysqli("localhost", "user", "pass", "leaderboard");

if ($conn->connect_error) {
    http_response_code(500);
    echo json_encode(["error" => "Database connection failed"]);
    exit();
}
  • Creates a connection to the MySQL database.
  • If the connection fails, returns 500 Internal Server Error.

Inserting or Updating Player Score

$sql = "INSERT INTO scores (player_name, score) VALUES (?, ?)
        ON DUPLICATE KEY UPDATE score = VALUES(score)";
$stmt = $conn->prepare($sql);
$stmt->bind_param("si", $playerName, $playerScore);
$stmt->execute();
$stmt->close();
$conn->close();
  • Prepares a SQL query to insert/update the player's score.
  • Uses ON DUPLICATE KEY UPDATE:
    • If the player exists → Updates their score.
    • If not → Inserts a new entry.
  • Prevents SQL injection by using bind_param("si", $playerName, $playerScore).

Setting Up PieSocket WebSocket Notification

$apiKey = "API_KEY";
$apiSecret = "API_SECRET";
$roomId = "leaderboard-channel";

$postData = [
    "key" => $apiKey,
    "secret" => $apiSecret,
    "roomId" => $roomId,
    "message" => [
        "event" => "scoreUpdate",
        "data" => [
            "name" => $playerName,
            "score" => $playerScore
        ]
    ]
];
  • Defines PieSocket API credentials ($apiKey, $apiSecret).
  • Creates a message payload to notify the leaderboard-channel about the updated score.

Sending WebSocket Notification via cURL

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL => "<https://free.blr2.piesocket.com/api/publish>",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_POSTFIELDS => json_encode($postData),
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json"
    ],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
  • Initializes cURL to send an HTTP POST request to PieSocket.
  • Sends the JSON-encoded postData.
  • Stores the response in $response and the HTTP status code in $httpCode.

Responding with JSON Output

echo json_encode([
    "status" => "success",
    "http_code" => $httpCode,
    "piesocket_response" => $response
]);
  • Sends a JSON response back to the frontend.
  • Includes:
    • "status": "success"
    • "http_code" from PieSocket response.
    • "piesocket_response" (WebSocket server response).

Setting HTTP Headers for CORS

header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Content-Type: application/json");
  • Access-Control-Allow-Origin: * → Allows requests from any domain.
  • Access-Control-Allow-Methods: GET, OPTIONS → Limits requests to GET (for fetching data) and OPTIONS (for CORS preflight).
  • Access-Control-Allow-Headers: Content-Type → Allows the client to send Content-Type in requests.
  • Content-Type: application/json → Ensures the response is JSON.

Connecting to the MySQL Database

$conn = new mysqli("localhost", "user", "pass", "leaderboard");

if ($conn->connect_error) {
    http_response_code(500);
    echo json_encode(["error" => "Database connection failed"]);
    exit();
}
  • Creates a MySQL database connection using mysqli.
  • If the connection fails, it:
    • Returns HTTP status 500 (Internal Server Error)
    • Sends a JSON error message: { "error": "Database connection failed" }
    • Exits the script to prevent further execution.

Querying the Leaderboard Data

$sql = "SELECT player_name, score FROM scores ORDER BY score DESC";
$result = $conn->query($sql);
  • The SQL query retrieves all player names and scores from the scores table.
  • Orders them by score DESC, meaning:
    • Highest scores appear first.
    • Players with the same score appear in their natural order.

Fetching & Formatting the Data

$scores = [];
while ($row = $result->fetch_assoc()) {
    $scores[] = $row;
}
  • Creates an empty array ($scores) to store results.
  • Loops through the query result:
    • Each row is fetched using $result->fetch_assoc().
    • The row is added to the $scores array.

Closing the Database Connection

$conn->close();
  • Closes the database connection to free up resources.

Returning JSON Response

echo json_encode(["status" => "success", "data" => $scores]);
  • Sends a JSON response containing:
    • "status": "success" → Indicates success.
    • "data": [...] → Contains an array of players and their scores.

Running The App:

Start a PHP server:

php -S 127.0.0.1:8000

And open the html file in a browser.

Complete Code

The project is available on our GitHub : https://github.com/piehostHQ/LeaderBoard

Comments

Leave a comment.

Share your thoughts or ask a question to be added in the loop.