Dynamic FAQ – PHP and MySQL

In this tutorial, we are going to teach you how to create a dynamic FAQ section on your website using PHP and MySQL. We will create simple CRUD (Create, Read, Update, Delete) operation using PHP PDO. FAQs will be added, updated, and deleted from the admin panel. And they will be displayed on the user side.

Usually, the FAQ section is displayed statically. But it would be great if you can manage it from your admin panel. That’s why we should choose dynamic FAQ section over hard-coded one.

Table of content:

  1. Add FAQ from the admin panel
  2. Display all FAQs to the admin
  3. Edit any FAQ
  4. Delete FAQ
  5. Display FAQs on the user side

We will be using PHP PDO prepared statements to prevent SQL injection. We will be using bootstrap, jQuery, font-awesome, and richText libraries in this tutorial. All source files can be downloaded at the end of this article. Download all these libraries and paste them into your project.

1. Add FAQ

First, we will create a file named “add.php“. In this file, we will first include the above libraries.

<!-- include bootstrap, font awesome and rich text library CSS -->
<link rel="stylesheet" type="text/css" href="css/bootstrap.css" />
<link rel="stylesheet" type="text/css" href="font-awesome/css/font-awesome.css" />
<link rel="stylesheet" type="text/css" href="richtext/richtext.min.css" />
<!-- include jquer, bootstrap and rich text JS -->
<script src="js/jquery-3.3.1.min.js"></script>
<script src="js/bootstrap.js"></script>
<script src="richtext/jquery.richtext.js"></script>

Then we will create a form that will display input fields for the question and its answer.

<!-- layout for form to add FAQ -->
<div class="container" style="margin-top: 50px; margin-bottom: 50px;">
	<div class="row">
		<div class="offset-md-3 col-md-6">
			<h1 class="text-center">Add FAQ</h1>
			<!-- for to add FAQ -->
			<form method="POST" action="add.php">
				<!-- question -->
				<div class="form-group">
					<label>Enter Question</label>
					<input type="text" name="question" class="form-control" required />
				</div>
				<!-- answer -->
				<div class="form-group">
					<label>Enter Answer</label>
					<textarea name="answer" id="answer" class="form-control" required></textarea>
				</div>
				<!-- submit button -->
				<input type="submit" name="submit" class="btn btn-info" value="Add FAQ" />
			</form>
		</div>
	</div>
	[show all FAQs here]
</div>

Then you need to initialize the richText library to make the textarea field with more options like text styles, fonts, lists, tables, and images, etc. Following Javascript code will do that:

// initialize rich text library
window.addEventListener("load", function () {
	$("#answer").richText();
});

Then we will handle this request in the same file. We will create the required table in the MySQL database dynamically. Then we will insert a new row with questions and answers entered in input fields.

<?php
	// connect with database
	$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");
	// check if insert form is submitted
	if (isset($_POST["submit"]))
	{
		// create table if not already created
		$sql = "CREATE TABLE IF NOT EXISTS faqs (
			id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
			question TEXT NULL,
			answer TEXT NULL,
			created_at DATETIME DEFAULT CURRENT_TIMESTAMP
		)";
		$statement = $conn->prepare($sql);
		$statement->execute();
		// insert in faqs table
		$sql = "INSERT INTO faqs (question, answer) VALUES (?, ?)";
		$statement = $conn->prepare($sql);
		$statement->execute([
			$_POST["question"],
			$_POST["answer"]
		]);
	}
	// [query to get all FAQs]
?>

Run the code now and you will be able to see a form with 2 input fields. When hit submit, you will see a new table will be created in your database. And a new row will be inserted in that table. Add a few more questions and their answers using the form.

2. Display all FAQ

Now we need to display all added FAQs to the admin side. In the same add.php file, the following code goes in the [query to get all FAQs] section:

// get all faqs from latest to oldest
$sql = "SELECT * FROM faqs ORDER BY id DESC";
$statement = $conn->prepare($sql);
$statement->execute();
$faqs = $statement->fetchAll();

This will fetch all the questions sorted by newest to oldest. Now we need to display all of them on a table. The following code goes in the [show all FAQs here] section:

<!-- show all FAQs added -->
<div class="row">
	<div class="offset-md-2 col-md-8">
		<table class="table table-bordered">
			<!-- table heading -->
			<thead>
				<tr>
					<th>ID</th>
					<th>Question</th>
					<th>Answer</th>
					<th>Actions</th>
				</tr>
			</thead>
			<!-- table body -->
			<tbody>
				<?php foreach ($faqs as $faq): ?>
					<tr>
						<td><?php echo $faq["id"]; ?></td>
						<td><?php echo $faq["question"]; ?></td>
						<td><?php echo $faq["answer"]; ?></td>
						<td>
							[edit button goes here]
							[delete button goes here]
						</td>
					</tr>
				<?php endforeach; ?>
			</tbody>
		</table>
	</div>
</div>

Refresh the page now and you will be able to view all the inserted questions sorting by newest to oldest.

3. Edit any FAQ

Following are the steps to update any specific row in the MySQL database using PHP PDO prepared statement:

  1. Show a link to go to edit page with each row
  2. Fetch the row data from MySQL using ID from URL
  3. Create a same form like add.php
  4. Auto-populate the values in the input fields
  5. Create a hidden input field for ID
  6. Update the data in database using ID
  7. Redirect to previous page

1. Show a link to go to edit page

Following code goes in the [edit button goes here] section:

<!-- edit button -->
<a href="edit.php?id=<?php echo $faq['id']; ?>" class="btn btn-warning btn-sm">
	Edit
</a>

Refresh the page now and you will see an edit link with each row. Then create a file named “edit.php” that will handle this request.

2. Fetch the row from MySQL

You will see an ID in the URL in the browser’s address bar. We need to get this value and search it in the MySQL database. This file will be similar to add.php but with auto-populate values in the input fields. First, we will fetch the row from the database.

<?php
	// connect with database
	$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");
	// check if FAQ exists
	$sql = "SELECT * FROM faqs WHERE id = ?";
	$statement = $conn->prepare($sql);
	$statement->execute([
		$_REQUEST["id"]
	]);
	$faq = $statement->fetch();
	if (!$faq)
	{
		die("FAQ not found");
	}
    // [update query goes here]
?>

This will fetch the row from the database using the ID from the URL. If the row is not found, then it will display an error and stop the script.

3, 4 & 5. Create form with auto-populate values

This form will use the above $faq variable to display the values in input fields.

<!-- include CSS -->
<link rel="stylesheet" type="text/css" href="css/bootstrap.css" />
<link rel="stylesheet" type="text/css" href="font-awesome/css/font-awesome.css" />
<link rel="stylesheet" type="text/css" href="richtext/richtext.min.css" />
<!-- include JS -->
<script src="js/jquery-3.3.1.min.js"></script>
<script src="js/bootstrap.js"></script>
<script src="richtext/jquery.richtext.js"></script>
<!-- layout for form to edit FAQ -->
<div class="container" style="margin-top: 50px; margin-bottom: 50px;">
	<div class="row">
		<div class="offset-md-3 col-md-6">
			<h1 class="text-center">Edit FAQ</h1>
			<!-- form to edit FAQ -->
			<form method="POST" action="edit.php">
				<!-- hidden ID field of FAQ -->
				<input type="hidden" name="id" value="<?php echo $faq['id']; ?>" required />
				<!-- question, auto-populate -->
				<div class="form-group">
					<label>Enter Question</label>
					<input type="text" name="question" class="form-control" value="<?php echo $faq['question']; ?>" required />
				</div>
				<!-- answer, auto-populate -->
				<div class="form-group">
					<label>Enter Answer</label>
					<textarea name="answer" id="answer" class="form-control" required><?php echo $faq['answer']; ?></textarea>
				</div>
				<!-- submit button -->
				<input type="submit" name="submit" class="btn btn-warning" value="Edit FAQ" />
			</form>
		</div>
	</div>
</div>
<script>
	// initialize rich text library
	window.addEventListener("load", function () {
		$("#answer").richText();
	});
</script>

If you refresh the page now, you will see the same form as the added FAQ but the input fields are auto-populated with selected FAQ values. It will also have a hidden input field for the ID.

6 & 7. Update the data and redirect back

Now we need to handle the update request. In the edit.php file, replace the [update query goes here] section with the following code to update the data in the database.

// check if edit form is submitted
if (isset($_POST["submit"]))
{
	// update the FAQ in database
	$sql = "UPDATE faqs SET question = ?, answer = ? WHERE id = ?";
	$statement = $conn->prepare($sql);
	$statement->execute([
		$_POST["question"],
		$_POST["answer"],
		$_POST["id"]
	]);
	// redirect back to previous page
	header("Location: " . $_SERVER["HTTP_REFERER"]);
}

Refresh the page now and try to update any FAQ. You will see that it will be updated in the MySQL database as well as in the HTML table too.

4. Delete FAQ

First, we need to show a delete button with each row. It will be a form with a hidden input field that contains the value of that FAQ. When that form is submitted, we will again first check if the record exists in the database. If yes, then we will simply remove it.

The following code goes in the [delete button goes here] section of the add.php file:

<!-- delete form -->
<form method="POST" action="delete.php" onsubmit="return confirm('Are you sure you want to delete this FAQ ?');">
	<input type="hidden" name="id" value="<?php echo $faq['id']; ?>" required />
	<input type="submit" value="Delete" class="btn btn-danger btn-sm" />
</form>

Then we need to create a new file named “delete.php” that will handle this request.

<?php
	// connect database
	$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");
	// check if FAQ existed
	$sql = "SELECT * FROM faqs WHERE id = ?";
	$statement = $conn->prepare($sql);
	$statement->execute([
		$_REQUEST["id"]
	]);
	$faq = $statement->fetch();
	if (!$faq)
	{
		die("FAQ not found");
	}
	// delete from database
	$sql = "DELETE FROM faqs WHERE id = ?";
	$statement = $conn->prepare($sql);
	$statement->execute([
		$_POST["id"]
	]);
	// redirect to previous page
	header("Location: " . $_SERVER["HTTP_REFERER"]);
?>

If you refresh the page now, you will see a red delete button with each row. On clicking, it will ask for confirmation. When confirmed, it will remove the FAQ from the MySQL database using PHP.

5. Display FAQs on the user side

Now comes the final part, where we will display our dynamic FAQ on the user side. Typically we have an index.php as our website home page. But you might have a separate page for FAQs. Either way, you first need to fetch all the data from the database:

<?php
	// connect with database
	$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");
	// fetch all FAQs from database
	$sql = "SELECT * FROM faqs";
	$statement = $conn->prepare($sql);
	$statement->execute();
	$faqs = $statement->fetchAll();
?>

After that, we will be using Bootstrap’s panel to display the data as an accordion.

<!-- include CSS -->
<link rel="stylesheet" type="text/css" href="css/bootstrap.css" />
<link rel="stylesheet" type="text/css" href="font-awesome/css/font-awesome.css" />
<!-- include JS -->
<script src="js/jquery-3.3.1.min.js"></script>
<script src="js/bootstrap.js"></script>
<!-- show all FAQs in a panel -->
<div class="container" style="margin-top: 50px; margin-bottom: 50px;">
	<div class="row">
		<div class="col-md-12 accordion_one">
		    <div class="panel-group">
		    	<?php foreach ($faqs as $faq): ?>
			        <div class="panel panel-default">
			        	<!-- button to show the question -->
			            <div class="panel-heading">
			                <h4 class="panel-title">
			                	<a data-toggle="collapse" data-parent="#accordion_oneLeft" href="#faq-<?php echo $faq['id']; ?>" aria-expanded="false" class="collapsed">
			                		<?php echo $faq['question']; ?>
			                	</a>
			                </h4>
			            </div>
			            <!-- accordion for answer -->
			            <div id="faq-<?php echo $faq['id']; ?>" class="panel-collapse collapse" aria-expanded="false" role="tablist" style="height: 0px;">
			                <div class="panel-body">
			                	<div class="text-accordion">
			                        <?php echo $faq['answer']; ?>
			                    </div>
			                </div>
			            </div>
			        </div>
		        <?php endforeach; ?>
		    </div>
		</div>
	</div>
</div>

Finally, you can apply some CSS styles to make it look better.

.accordion_one .panel-group {
    border: 1px solid #f1f1f1;
    margin-top: 100px
}
a:link {
    text-decoration: none
}
.accordion_one .panel {
    background-color: transparent;
    box-shadow: none;
    border-bottom: 0px solid transparent;
    border-radius: 0;
    margin: 0
}
.accordion_one .panel-default {
    border: 0
}
.accordion-wrap .panel-heading {
    padding: 0px;
    border-radius: 0px
}
h4 {
    font-size: 18px;
    line-height: 24px
}
.accordion_one .panel .panel-heading a.collapsed {
    color: #999999;
    display: block;
    padding: 12px 30px;
    border-top: 0px
}
.accordion_one .panel .panel-heading a {
    display: block;
    padding: 12px 30px;
    background: #fff;
    color: #313131;
    border-bottom: 1px solid #f1f1f1
}
.accordion-wrap .panel .panel-heading a {
    font-size: 14px
}
.accordion_one .panel-group .panel-heading+.panel-collapse>.panel-body {
    border-top: 0;
    padding-top: 0;
    padding: 25px 30px 30px 35px;
    background: #fff;
    color: #999999
}
.img-accordion {
    width: 81px;
    float: left;
    margin-right: 15px;
    display: block
}
.accordion_one .panel .panel-heading a.collapsed:after {
    content: "\2b";
    color: #999999;
    background: #f1f1f1
}
.accordion_one .panel .panel-heading a:after,
.accordion_one .panel .panel-heading a.collapsed:after {
    font-family: 'FontAwesome';
    font-size: 15px;
    width: 36px;
    line-height: 48px;
    text-align: center;
    background: #F1F1F1;
    float: left;
    margin-left: -31px;
    margin-top: -12px;
    margin-right: 15px
}
.accordion_one .panel .panel-heading a:after {
    content: "\2212"
}
.accordion_one .panel .panel-heading a:after,
.accordion_one .panel .panel-heading a.collapsed:after {
    font-family: 'FontAwesome';
    font-size: 15px;
    width: 36px;
    height: 48px;
    line-height: 48px;
    text-align: center;
    background: #F1F1F1;
    float: left;
    margin-left: -31px;
    margin-top: -12px;
    margin-right: 15px
}

Run the code now and you will see a beautiful accordion with a question, which when clicked will display an answer. You can add as many questions and their answer from the database and they will be displayed here automatically.

So that’s how you can create a dynamic FAQ section on your website that can be managed from your admin panel.

Learn how to add dynamic testimonial section on your website from here.

Source code:

Dynamic testimonial – PHP & MySQL, Vue JS

In this tutorial, we are going to teach you how you can add a dynamic testimonial to your website using PHP and MySQL in the backend and Vue JS in the frontend. Testimonials are displayed on a website to show the new users how your product satisfies your previous customers. We will be creating 2 pages, one for adding testimonials from the admin panel. And second to display all testimonials in a beautiful design.

If you do not have a dynamic testimonial, then you have to manually add, modify or delete a testimonial from your website. By going dynamic, you can perform all these actions from your admin panel.

Add Testimonial

First, download Bootstrap from here and Vue JS from here. Paste the CSS and JS files in your project, we will be using them in a moment. After that, we need to create a form from which we can add testimonials. Each testimonial will have a picture of the person, name, designation in his company, and his comments.

The following code goes in your admin panel from where you want to add testimonials.

<!-- include bootstrap -->
<link rel="stylesheet" type="text/css" href="bootstrap.min.css" />
 
<!-- include vue js -->
<script src="vue.min.js"></script>

<!-- container for vue js app -->
<div class="container" style="margin-top: 50px; margin-bottom: 50px;" id="addTestimonialApp">
    <div class="row">
        <!-- center align form -->
        <div class="offset-md-3 col-md-6">
            <h2 style="margin-bottom: 30px;">Add Testimonial</h2>
 
 			<!-- form to add testimonial -->
            <form v-on:submit.prevent="store" enctype="multipart/form-data">

            	<!-- picture of user -->
                <div class="form-group">
                    <label>Picture</label>
                    <input type="file" name="picture" accept="image/*" class="form-control" />
                </div>

                <!-- name of user -->
                <div class="form-group">
                    <label>Name</label>
                    <input type="text" name="name" class="form-control" />
                </div>

                <!-- designation of user -->
                <div class="form-group">
                    <label>Designation</label>
                    <input type="text" name="designation" class="form-control" />
                </div>

                <!-- comment -->
                <div class="form-group">
                    <label>Comment</label>
                    <textarea name="comment" class="form-control"></textarea>
                </div>

                <!-- submit button -->
                <input type="submit" name="submit" class="btn btn-info" value="Add Testimonial" />
            </form>
        </div>
    </div>

    [show all testimonials for deleting]
</div>

This will show a form with input fields. But when you click on the “Add Testimonial” button, nothing happens. This is because we need to render it using Vue JS.

<script>
    // initialize vue js app
    var addTestimonialApp = new Vue({
        el: "#addTestimonialApp", // id of container div
        data: {
            // all values used in this app
            testimonials: []
        },
        // all methods
        methods: {
            // [other methods goes here]

             // called when form is submitted
            store: function () {
                // get this app instance
                var self = this;
                var form = event.target;
             
                // call an AJAX to create a new entry in testimonials
                var ajax = new XMLHttpRequest();
                ajax.open("POST", "store.php", true);
             
                ajax.onreadystatechange = function () {
                    if (this.readyState == 4) { // response received
                        if (this.status == 200) { // response is successfull
                            // console.log(this.responseText);
             
                            // parse the response from JSON string to JS arrays and objects
                            var response = JSON.parse(this.responseText);
                            // console.log(response);

                            alert(response.message);
             
                            // if there is no error
                            if (response.status == "success") {
                                self.testimonials.unshift(response.testimonial);
                                form.reset();
                            } else {
                                // when there is any error
                            }
                        }
             
                        if (this.status == 500) {
                            console.log(this.responseText);
                        }
                    }
                };
             
                // create form data object and form to it
                var formData = new FormData(form);
             
                // actually sending the request
                ajax.send(formData);
            },
        },

        // [mount code goes here]
    });
</script>

Refresh the page now and you will be able to submit the form, this is because of Vue JS. An AJAX request will be sent to the server to store the picture attached and save the other fields in the MySQL database using PHP.

Create a new file named “store.php” and paste the following code in it:

<?php
 
// connect with database
$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");

// create tables if not exists
$sql = "CREATE TABLE IF NOT EXISTS testimonials (
    id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
    picture TEXT NULL,
    name VARCHAR(255) NULL,
    designation VARCHAR(255) NULL,
    comment TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)";
$result = $conn->prepare($sql);
$result->execute();

$file_path = "";
if ($_FILES["picture"]["error"] == 0)
{
    $folder_name = "testimonials";
    mkdir($folder_name);
    $file_path = $folder_name . "/" . $_FILES["picture"]["name"];
    move_uploaded_file($_FILES["picture"]["tmp_name"], $file_path);
}

// insert in testimonials table
$sql = "INSERT INTO testimonials (picture, name, designation, comment, created_at) VALUES (?, ?, ?, ?, NOW())";
$result = $conn->prepare($sql);
$result->execute([
    $file_path,
    $_POST["name"],
    $_POST["designation"],
    $_POST["comment"],
]);
$testimonial_id = $conn->lastInsertId();

// get the testimonial just inserted in database
$sql = "SELECT * FROM testimonials WHERE id = ?";
$result = $conn->prepare($sql);
$result->execute([
    $testimonial_id
]);
$testimonial = $result->fetch();

echo json_encode([
    "status" => "success",
    "message" => "Testimonial has been added.",
    "testimonial" => $testimonial
]);
exit();

If you refresh the page now, upload the picture, enter the fields and hit submit, it will create a new table in the database if not already created. Then it will create a folder named “testimonials” and save the image file in it. Then it will insert a new row in it. And finally, it will return the new row back to the client (AJAX).

From there we will prepend it in our local testimonials array. Now we need to display all the added testimonials in an HTML table with a button to delete them.

Display all Testimonials to Admin

The following code goes in the [show all testimonials for deleting] section:

<!-- show all testimonials -->
<div class="row" style="margin-top: 50px;">
    <div class="col-md-12">
        <table class="table table-bordered">
            <!-- table heading -->
            <tr>
                <th>ID</th>
                <th>Picture</th>
                <th>Name</th>
                <th>Designation</th>
                <th>Comment</th>
                <th>Actions</th>
            </tr>
 
            <!-- loop through an array of testimonials -->
            <tr v-for="(testimonial, index) in testimonials">
                <td v-text="testimonial.id"></td>
                <td>
                    <img v-bind:src="testimonial.picture" style="width: 300px;" />
                </td>
                <td v-text="testimonial.name"></td>
                <td v-text="testimonial.designation"></td>
                <td v-text="testimonial.comment"></td>
                <td>
                    <!-- form to delete testimonial -->
                    <form v-on:submit.prevent="deleteTestimonial">
                        <input type="hidden" name="id" v-model="testimonial.id" required />
                        <input type="submit" class="btn btn-danger btn-sm" value="Delete" />
                    </form>
                </td>
            </tr>
        </table>
    </div>
</div>

This will create an empty HTML table because we need to load the in it first. We will call an AJAX to fetch all the stored testimonials using PHP and MySQL and display them using Vue JS. The following code goes in the [mount code goes here] section:

mounted: function () {
    this.getData();
}

Now we need to create a function named “getData” in our Vue JS instance. Replace the code in section [other methods goes here] with the following:

// [delete method]

getData: function () {
    // get this app instance
    var self = this;
 
    // call an AJAX to get all testimonials
    var ajax = new XMLHttpRequest();
    ajax.open("POST", "fetch.php", true);
 
    ajax.onreadystatechange = function () {
        if (this.readyState == 4) { // response received
            if (this.status == 200) { // response is successfull
                // console.log(this.responseText);
 
                // parse the response from JSON string to JS arrays and objects
                var response = JSON.parse(this.responseText);
                // console.log(response);
 
                // if there is no error
                if (response.status == "success") {
                    self.testimonials = response.data;
                } else {
                    // when there is any error
                }
            }
 
            if (this.status == 500) {
                console.log(this.responseText);
            }
        }
    };
 
    // create form data object
    var formData = new FormData();
 
    // actually sending the request
    ajax.send(formData);
},

Finally, we need to create a new file named “fetch.php” that will fetch all the testimonials from the MySQL database using PHP.

<?php

// connect with database
$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");

// fetch all testimonials
$sql = "SELECT * FROM testimonials ORDER BY id DESC";
$statement = $conn->prepare($sql);
$statement->execute();
$data = $statement->fetchAll();

// create new field for full comment text
// because we will be displaying less text and display 'show more' button
for ($a = 0; $a < count($data); $a++)
{
	$data[$a]["comment_full"] = $data[$a]["comment"];
	$data[$a]["comment"] = substr($data[$a]["comment"], 0, 50);
}

// send the response back to client
echo json_encode([
    "status" => "success",
    "message" => "Testimonial has been fetched.",
    "data" => $data
]);
exit();

?>

Refresh the page now and you will be able to see all the testimonials added. Also, if you add a new testimonial, it will automatically be prepended in the HTML table. Now we need to make it able to delete the testimonial.

Delete Testimonial

We need to follow the following steps to delete the testimonial:

  1. Call an AJAX with an ID of testimonial.
  2. On server side, fetch the testimonial using ID.
  3. Delete the picture from the “testimonials” folder using PHP unlink() function.
  4. Delete the testimonial from MySQL database.
  5. Send the response back to client.
  6. The client will remove the testimonial from local array.
  7. It will automatically be removed from the HTML table.

Replace the section [delete method] with the following code:

// method to delete testimonial
deleteTestimonial: function () {
    // get this app instance
    var self = this;
 
    // get form
    var form = event.target;
 
    // call an AJAX to delete the testimonial
    var ajax = new XMLHttpRequest();
    ajax.open("POST", "delete.php", true);
 
    ajax.onreadystatechange = function () {
        if (this.readyState == 4) { // response received
            if (this.status == 200) { // response is successfull
                // console.log(this.responseText);
 
                // parse the response from JSON string to JS arrays and objects
                var response = JSON.parse(this.responseText);
                console.log(response);
 
                // remove from local array if deleted from server
                if (response.status == "success") {
                    for (var a = 0; a < self.testimonials.length; a++) {
                        var testimonial = self.testimonials[a];
                        if (testimonial.id == form.id.value) {
                            self.testimonials.splice(a, 1);
                            break;
                        }
                    }
                } else {
                    // display an error message
                    alert(response.message);
                }
            }
 
            if (this.status == 500) {
                console.log(this.responseText);
            }
        }
    };
 
    // append form in form data object
    var formData = new FormData(form);
 
    // call AJAX with form data
    ajax.send(formData);
},

Then we need to create a new file named “delete.php” that will handle this request. It will have the following code:

<?php
 
// connect with database
$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");

// get the testimonial just inserted in database
$sql = "SELECT * FROM testimonials WHERE id = ?";
$result = $conn->prepare($sql);
$result->execute([
    $_POST["id"]
]);
$testimonial = $result->fetch();

if (!$testimonial)
{
    // send the response back to client
    echo json_encode([
        "status" => "error",
        "message" => "Testimonial not found."
    ]);
    exit();
}

// remove the picture from folder
unlink($testimonial["picture"]);

// create a query to delete the pricing table from database
$sql = "DELETE FROM testimonials WHERE id = ?";
 
// prepare the query
$result = $conn->prepare($sql);
 
// execute the query
$result->execute([
    $_POST["id"]
]);
 
// send the response back to client
echo json_encode([
    "status" => "success",
    "message" => "Testimonial has been deleted."
]);
exit();

Refresh the page now and you will be able to delete the testimonials as well. The only thing left is to show the testimonials on the user side.

Display Testimonials on User Side

To display testimonials on the user side, you need to download font-awesome and slick, you already have the Bootstrap and Vue JS files in your project folder. You can download font-awesome from here and slick from here. After downloading, paste both folders into your project, we will include them on your user side.

The following code will display the layout for each testimonial using HTML. Which we will render using Vue JS in the next step.

<!-- include bootstrap CSS -->
<link rel="stylesheet" type="text/css" href="bootstrap.min.css" />

<!-- include font awesome -->
<link rel="stylesheet" type="text/css" href="font-awesome/css/font-awesome.min.css" />

<!-- include slick -->
<link rel="stylesheet" type="text/css" href="slick.css" />
<link rel="stylesheet" type="text/css" href="slick-theme.css" />

<!-- include vue js -->
<script src="vue.min.js"></script>

<div class="container" id="testimonialApp" style="margin-top: 30px;">
	<div class="row">
		<div class="col-md-12">
			<h2 class="text-center">Testimonials</h2>
		</div>
	</div>

	<div class="row">
		<div class="col-md-12">
			<div class="items">

				<div class="card" v-for="(testimonial, index) in testimonials">
			        <div class="card-body">
			            <h4 class="card-title">
			            	<img src="https://img.icons8.com/ultraviolet/40/000000/quote-left.png" />
			            </h4>
			            
			            <div class="template-demo">
			                <p>
			                	<span v-text="testimonial.comment"></span>

			                	<span class="show-more-text" v-on:click="loadMoreContent" v-bind:data-index="index">show more</span>
			                </p>
			            </div>

			            <h4 class="card-title">
			            	<img src="https://img.icons8.com/ultraviolet/40/000000/quote-right.png" style="margin-left: auto;" />
			            </h4>
			            
			            <hr />
			            
			            <div class="row">
			                <div class="col-sm-3">
			                	<img class="profile-pic" v-bind:src="testimonial.picture" />
			                </div>
			                
			                <div class="col-sm-9">
			                    <div class="profile">
			                        <h4 class="cust-name" v-text="testimonial.name"></h4>
			                        <p class="cust-profession" v-text="testimonial.designation"></p>
			                    </div>
			                </div>
			            </div>
			        </div>
			    </div>
			    
			</div>
		</div>
	</div>
</div>

<!-- include jquery -->
<script src="jquery-3.3.1.min.js"></script>

<script src="slick.min.js"></script>

<!-- include bootstrap JS -->
<script src="bootstrap.min.js"></script>

<!-- your JS code -->
<script src="script.js?v=<?php echo time(); ?>"></script>

Create a new file named “script.js” and paste the following code in it:

var mainURL = window.location.origin + "/" + window.location.pathname + "/";

var testimonialApp = new Vue({
    el: "#testimonialApp",
    data: {
        testimonials: []
    },
    methods: {
        loadMoreContent: function () {
            var node = event.target;
            var index = node.getAttribute("data-index");

            if (this.testimonials[index].comment.length > 50) {
                // it needs to display less
                node.innerHTML = "show more";
                this.testimonials[index].comment = this.testimonials[index].comment_full.substr(0, 50);
            } else {
                // it needs to display more
                node.innerHTML = "show less";
                this.testimonials[index].comment = this.testimonials[index].comment_full;
            }
        },

        getData: function () {
            // get this app instance
            var self = this;
         
            // call an AJAX to get all testimonials
            var ajax = new XMLHttpRequest();
            ajax.open("POST", "fetch.php", true);
         
            ajax.onreadystatechange = function () {
                if (this.readyState == 4) { // response received
                    if (this.status == 200) { // response is successfull
                        // console.log(this.responseText);
         
                        // parse the response from JSON string to JS arrays and objects
                        var response = JSON.parse(this.responseText);
                        // console.log(response);
         
                        // if there is no error
                        if (response.status == "success") {
                            self.testimonials = response.data;

                            setTimeout(function () {
                                $('.items').slick({
                                    dots: true,
                                    infinite: true,
                                    speed: 800,
                                    autoplay: false,
                                    slidesToShow: 2,
                                    slidesToScroll: 2,
                                    responsive: [{
                                            breakpoint: 1024,
                                            settings: {
                                                slidesToShow: 3,
                                                slidesToScroll: 3,
                                                infinite: true,
                                                dots: true
                                            }
                                        }, {
                                            breakpoint: 600,
                                            settings: {
                                                slidesToShow: 2,
                                                slidesToScroll: 2
                                            }
                                        }, {
                                            breakpoint: 480,
                                            settings: {
                                                slidesToShow: 1,
                                                slidesToScroll: 1
                                            }
                                        }
                                    ]
                                });
                            }, 100);
                        } else {
                            // when there is any error
                        }
                    }
         
                    if (this.status == 500) {
                        console.log(this.responseText);
                    }
                }
            };
         
            // create form data object
            var formData = new FormData();
         
            // actually sending the request
            ajax.send(formData);
        }
    },
    mounted: function () {
        this.getData();
    }
});

At this point, you will be able to view the testimonials added from the admin panel on your website. It will also have a carousel slider. Finally, you can apply some CSS styles to make it look better for the users.

<style>
	.show-more-text {
		background-color: #72a4d5;
	    color: white;
	    padding: 3px 5px;
	    border-radius: 5px;
	    margin-left: 3px;
	    cursor: pointer;
	}
	.more {
		display: none;
	}

	@media (max-width:991.98px) {
		.padding {
			padding: 1.5rem
		}
	}

	@media (max-width:767.98px) {
		.padding {
			padding: 1rem
		}
	}

	.padding {
		padding: 5rem
	}

	.card {
		position: relative;
		display: flex;
		width: 350px;
		flex-direction: column;
		min-width: 0;
		word-wrap: break-word;
		background-color: #fff;
		background-clip: border-box;
		border: 1px solid #d2d2dc;
		border-radius: 11px;
		-webkit-box-shadow: 0px 0px 5px 0px rgb(249, 249, 250);
		-moz-box-shadow: 0px 0px 5px 0px rgba(212, 182, 212, 1);
		box-shadow: 0px 0px 5px 0px rgb(161, 163, 164)
	}

	.card .card-body {
		padding: 1rem 1rem
	}

	.card-body {
		flex: 1 1 auto;
		padding: 1.25rem
	}

	p {
		font-size: 0.875rem;
		margin-bottom: .5rem;
		line-height: 1.5rem
	}

	h4 {
		line-height: .2 !important
	}

	.profile {
		margin-top: 16px;
		margin-left: 11px
	}

	.profile-pic {
		width: 100px;
	}

	.cust-name {
		font-size: 18px
	}

	.cust-profession {
		font-size: 10px
	}

	.items {
		width: 90%;
		margin: 0px auto;
		margin-top: 30px
	}

	.slick-slide {
		margin: 10px;
		height: auto !important;
	}
</style>

So that’s it, you have a dynamic testimonial section fully manageable from the admin panel. If you face any problems in following this, kindly do let us know in the comments section below.

Also learn how to add dynamic pricing table in your website.

Download

[wpdm_package id=’1399′]

Dynamic pricing table – PHP, MySQL, and Vue JS

A pricing table on a website is used to give your users an idea of how much you charge for your services. In this article, we are going to teach you, how you can create a dynamic pricing table using PHP, MySQL, and Vue JS. The pricing table is created in bootstrap so it will be mobile responsive.

From the admin panel, we will create multiple pricing tables and their data will be stored in the MySQL database. On the user side, the data will be fetched from the database and displayed to the user using Vue JS.

Pricing tables are basically packages offered at different prices so users can pick the one they find convenient. We will be using PHP as a server-side language, and MySQL to store the pricing table data in the database. And Vue JS to render the HTML.

Show form to add pricing table

To make our pricing table dynamic, we must first add it in the database. First, download Bootstrap from here and Vue JS from here. Paste the CSS and JS files in your project, we will be using them in a moment. After that, we need to create a form from which we can create a pricing table.

Each pricing table will have a title, a short description, an amount, and some features. The feature means the services you will be provided in that package.

The following code goes in your admin panel from where you want to add pricing tables.

<!-- include bootstrap -->
<link rel="stylesheet" type="text/css" href="bootstrap.min.css" />

<!-- include vue js -->
<script src="vue.min.js"></script>

<!-- container for vue js app -->
<div class="container" style="margin-top: 50px; margin-bottom: 50px;" id="addPricingTableApp">
	<div class="row">
		<!-- center align form -->
		<div class="offset-md-3 col-md-6">
			<h2 style="margin-bottom: 30px;">Create Pricing Table</h2>

			<!-- prevent the form from redirecting, and call JS function 'store' -->
			<form v-on:submit.prevent="store">

				<!-- get title of pricing table -->
				<div class="form-group">
                    <label>Title</label>
                    <input type="text" v-model="title" class="form-control" />
                </div>

                <!-- get description -->
                <div class="form-group">
                    <label>Description</label>
                    <input type="text" v-model="description" class="form-control" />
                </div>

                <!-- get amount -->
                <div class="form-group">
                    <label>Amount</label>
                    <input type="number" v-model="amount" class="form-control" />
                </div>

                <!-- list of features in this pricing table -->
                <h2 style="margin-top: 50px; margin-bottom: 50px;">Features</h2>

                <div class="row" style="margin-bottom: 50px;">
                    <div class="col-md-12">
                    	<!-- loop through an array in vue js app -->
                        <div v-for="(feature, index) in features" style="border: 1px solid black; padding: 10px; margin-bottom: 10px;">
                        	<!-- show input field to get the feature value -->
                            <div class="form-group">
                                <label>Feature</label>
                                <input type="text" class="form-control" v-model="features[index].value" placeholder="Can have HTML" />
                            </div>

                            <!-- button to delete the feature from pricing table -->
                            <button type="button" class="btn btn-danger btn-sm" v-on:click="deleteFeature" v-bind:data-index="index">Delete Feature</button>
                        </div>
                    </div>
                </div>

                <!-- button to add new feature -->
                <div class="row" style="margin-bottom: 20px;">
                    <div class="col-md-12">
                        <button type="button" class="btn btn-success" v-on:click="addFeature">Add Feature</button>
                    </div>
                </div>

                <!-- submit button -->
                <input type="submit" class="btn btn-info" value="Add Pricing" />
			</form>
		</div>
	</div>

	[show all pricing tables]
</div>

This will show a form with input fields. But when you click on the “Add Feature” button, nothing happens. This is because we need to render it using Vue JS.

<script>
	// initialize vue js app
    var addPricingTableApp = new Vue({
        el: "#addPricingTableApp", // id of container div
        data: {
        	// all values used in this app
        	title: "",
        	description: "",
        	amount: 0,
            features: [],
            pricings: []
        },
        // all methods
        methods: {
            
            // [store method]

        	// delete feature box
        	deleteFeature: function () {
        		var index = event.target.getAttribute("data-index");
        		this.features.splice(index, 1);
        	},
        	// add new feature box
        	addFeature: function() {
		        this.features.push({
		        	value: ""
		        });
		    }
        }
    });
</script>

Refresh the page now and you will be able to add and remove features, this is because of Vue JS. Now we need to call an AJAX request to save this pricing table data in the MySQL database so it can be fetched later.

Save pricing table in MySQL using PHP

First, we need to create a “store” method in Vue JS “methods” object. The following code goes in the [store method] section:

// called when form is submitted
store: function () {
    // get this app instance
    var self = this;

    // call an AJAX to create a new pricing table
    var ajax = new XMLHttpRequest();
    ajax.open("POST", "store.php", true);

    ajax.onreadystatechange = function () {
        if (this.readyState == 4) { // response received
            if (this.status == 200) { // response is successfull
                // console.log(this.responseText);

                // parse the response from JSON string to JS arrays and objects
                var response = JSON.parse(this.responseText);
                console.log(response);

                // if added in database, then prepend in local array too
                if (response.status == "success") {
                    self.pricings.unshift(response.pricing);
                } else {
                    // display an error message
                    alert(response.message);
                }
            }

            if (this.status == 500) {
                console.log(this.responseText);
            }
        }
    };

    // create form data object and append all the values in it
    var formData = new FormData();
    formData.append("title", this.title);
    formData.append("description", this.description);
    formData.append("amount", this.amount);
    formData.append("features", JSON.stringify(this.features));

    // actually sending the request
    ajax.send(formData);
},

Then we need to create a file named “store.php” that will handle this request. Following will be the code of the store.php file:

<?php

// connect with database
$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");

// create tables if not exists
$sql = "CREATE TABLE IF NOT EXISTS pricings (
	id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
	title VARCHAR(255) NOT NULL,
	description TEXT NULL,
	amount DOUBLE NOT NULL
)";
$result = $conn->prepare($sql);
$result->execute();

$sql = "CREATE TABLE IF NOT EXISTS pricing_features (
	id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
	pricing_id INTEGER,
	value TEXT NOT NULL,
	FOREIGN KEY (pricing_id) REFERENCES pricings(id) ON UPDATE CASCADE ON DELETE CASCADE
)";
$result = $conn->prepare($sql);
$result->execute();

// insert in pricings table
$sql = "INSERT INTO pricings (title, description, amount) VALUES (:title, :description, :amount)";
$result = $conn->prepare($sql);
$result->execute([
	":title" => $_POST["title"],
	":description" => $_POST["description"],
	":amount" => $_POST["amount"]
]);
$pricing_id = $conn->lastInsertId();

// save all features in a separate table
$features = json_decode($_POST["features"]);
foreach ($features as $feature)
{
	$sql = "INSERT INTO pricing_features(pricing_id, value) VALUES (:pricing_id, :value)";
	$result = $conn->prepare($sql);
	$result->execute([
		":pricing_id" => $pricing_id,
		":value" => $feature->value
	]);
}

// get the pricing table just inserted in database
$sql = "SELECT * FROM pricings WHERE id = :id";
$result = $conn->prepare($sql);
$result->execute([
	":id" => $pricing_id
]);
$pricing = $result->fetch();

// send the response back to client with new pricing table
echo json_encode([
	"status" => "success",
	"message" => "Pricing table has been added.",
	"pricing" => $pricing
]);
exit();

This assumes that you have a database named “test”. You can change it as per yours if you already have one. This will create the pricings and pricing_features table if you do not already have one.

Refresh the page now and fill out the pricing table form and hit submit. Then refresh your phpMyAdmin page and you will see a new row in the “pricings” table. You will also see multiple rows in the “pricing_features” table.

Show pricing table on the user side

Now the pricing table and its features have been saved in the MySQL database, you can easily display them on your user side. Paste the following code anywhere you want to show the pricing table to your users:

<link rel="stylesheet" type="text/css" href="bootstrap.min.css" />
<script src="vue.min.js"></script>

<style>
	.box-shadow {
		box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
	}
</style>

<div class="container" style="margin-top: 50px; margin-bottom: 50px;" id="pricingTableApp">
	<div class="row">
		<div class="col-md-12">
			<h2 class="text-center" style="margin-bottom: 30px;">Pricing Table</h2>
		</div>
	</div>

	<div class="row">
		<div class="col-md-3" v-for="(pricing, index) in pricings">

			<div class="card mb-4 box-shadow">
				<div class="card-header">
					<h4 class="my-0 font-weight-normal text-center" v-text="pricing.title"></h4>
				</div>

				<div class="card-body">
					<h1 class="card-title pricing-card-title text-center" v-text="'$' + pricing.amount"></h1>
					
					<ul class="list-unstyled mt-3 mb-4">
						<li v-for="(feature, featureIndex) in pricing.features" class="text-center">
							<span v-html="feature.value"></span>
						</li>
					</ul>

					<button type="button" class="btn btn-lg btn-block btn-outline-primary">Buy now</button>
				</div>
	        </div>
		</div>
	</div>
</div>

We need to call an AJAX request to get the pricing tables from the server. So following will be your Javascript code on same file:

<script>
    var pricingTableApp = new Vue({
        el: "#pricingTableApp",
        data: {
            pricings: []
        },
        methods: {
        	getData: function () {
        		var self = this;
        		var ajax = new XMLHttpRequest();
        		ajax.open("POST", "get-data.php", true);

        		ajax.onreadystatechange = function () {
        			if (this.readyState == 4) {
        				if (this.status == 200) {
        					// console.log(this.responseText);
        					var response = JSON.parse(this.responseText);
        					console.log(response);

        					if (response.status == "success") {
        						self.pricings = response.pricings;
        					}
        				}

        				if (this.status == 500) {
        					console.log(this.responseText);
        				}
        			}
        		};

        		var formData = new FormData();
        		ajax.send(formData);
        	}
        },
        mounted: function () {
        	this.getData();
        }
    });
</script>

Now create a file named “get-data.php” that will fetch pricing table along with their features and return to the client.

<?php

// connect with database
$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");

// get all pricing tables sorting by amount from lowest to highest
$sql = "SELECT * FROM pricings ORDER BY amount ASC";
$result = $conn->prepare($sql);
$result->execute();
$pricings = $result->fetchAll();

// get all the features of each pricing table too
$data = [];
foreach ($pricings as $pricing)
{
	$sql = "SELECT * FROM pricing_features WHERE pricing_id = :pricing_id";
	$result = $conn->prepare($sql);
	$result->execute([
		":pricing_id" => $pricing["id"]
	]);
	$pricing_features = $result->fetchAll();

	$pricing["features"] = $pricing_features;
	array_push($data, $pricing);
}

// send the response back to client with the data
echo json_encode([
	"status" => "success",
	"message" => "Data has been fetched.",
	"pricings" => $data
]);
exit();

Refresh the page now and you will be able to see a beautifully designed pricing table on your website.

Edit & delete the pricing table

We will have a pure dynamic pricing table if we were able to modify or delete the features from it from admin panel.

At this point, your desired work is work. But you can go ahead and allow an admin to edit or delete the pricing table. Suppose you want to add more features to some package, or you want to delete a specific pricing table altogether.

Display all pricing tables

First, we need to display a list of all pricing tables in an HTML table. The following code goes in the [show all pricing tables] section:

<!-- show all pricing tables -->
<div class="row" style="margin-top: 50px;">
	<div class="col-md-12">
		<table class="table table-bordered">
			<!-- table heading -->
			<tr>
				<th>ID</th>
				<th>Title</th>
				<th>Description</th>
				<th>Actions</th>
			</tr>

			<!-- loop through an array of pricing tables -->
			<tr v-for="(pricing, index) in pricings">
				<td v-text="pricing.id"></td>
				<td v-text="pricing.title"></td>
				<td v-text="pricing.description"></td>
				<td>
					<!-- edit pricing table link -->
					<a class="btn btn-link" v-bind:href="'edit.php?id=' + pricing.id">
						Edit
					</a>

					<!-- form to delete pricing table -->
					<form v-on:submit.prevent="deletePricing">
						<input type="hidden" name="id" v-model="pricing.id" required />
						<input type="submit" class="btn btn-danger btn-sm" value="Delete" />
					</form>
				</td>
			</tr>
		</table>
	</div>
</div>

We already have a Vue JS instance named addPricingTableApp. When this instance is mounted, we will call an AJAX request to get the pricing tables from MySQL database using PHP.

var addPricingTableApp = new Vue({
    el: "#addPricingTableApp",
    data: {
    	// other variables
    },
    methods: {
    	// other methods

    	// method to get all pricing tables from server
    	getData: function () {
    		// get this app instance
    		var self = this;

    		// call an AJAX to fetch all pricing tables
    		var ajax = new XMLHttpRequest();
    		ajax.open("POST", "get-data.php", true);

    		ajax.onreadystatechange = function () {
    			if (this.readyState == 4) { // response received
    				if (this.status == 200) { // response is successfull
    					// console.log(this.responseText);

    					// parse the response from JSON string to JS arrays and objects
    					var response = JSON.parse(this.responseText);
    					console.log(response);

    					// render the data in table
    					if (response.status == "success") {
    						self.pricings = response.pricings;
    					}
    				}

    				if (this.status == 500) {
    					console.log(this.responseText);
    				}
    			}
    		};

    		// send an AJAX request to the server
    		var formData = new FormData();
    		ajax.send(formData);
    	},

    	// method to delete pricing table
    	deletePricing: function () {
    		//
    	},
    	
    	// other methods
    },
    mounted: function () { // called when the DOM is mounted
    	this.getData();
    }
});

Refresh the admin side now and you will be able to view all the pricing tables in an HTML table. We already have a separate file for fetching data from the database “get-data.php“, so it will fetch easily.

Update

The table above created will also display edit and a delete button. When you click on the edit button, it will take you to the “edit.php” file with a parameter having the value of that pricing table. Now we need to create a file named “edit.php” which will show a form with an auto-populated value of that pricing table.

The edit page will also have the functionality to add or delete features from the pricing table. We will also be using Vue JS here to populate the values in input fields.

<link rel="stylesheet" type="text/css" href="bootstrap.min.css" />
<script src="vue.min.js"></script>

<div class="container" style="margin-top: 50px; margin-bottom: 50px;" id="editPricingTableApp">
	<div class="row">
		<div class="offset-md-3 col-md-6">
			<h2 style="margin-bottom: 30px;">Edit Pricing Table</h2>

			<form v-on:submit.prevent="update" v-if="pricing != null">
				<div class="form-group">
                    <label>Title</label>
                    <input type="text" v-model="pricing.title" class="form-control" />
                </div>

                <div class="form-group">
                    <label>Description</label>
                    <input type="text" v-model="pricing.description" class="form-control" />
                </div>

                <div class="form-group">
                    <label>Amount</label>
                    <input type="number" v-model="pricing.amount" class="form-control" />
                </div>

                <h2 style="margin-top: 50px; margin-bottom: 50px;">Features</h2>

                <div class="row" style="margin-bottom: 50px;">
                    <div class="col-md-12">
                        <div v-for="(feature, index) in pricing.features" style="border: 1px solid black; padding: 10px; margin-bottom: 10px;">
                            <div class="form-group">
                                <label>Feature</label>
                                <input type="text" class="form-control" v-model="pricing.features[index].value" placeholder="Can have HTML" />
                            </div>

                            <button type="button" class="btn btn-danger btn-sm" v-on:click="deleteFeature" v-bind:data-index="index">Delete Feature</button>
                        </div>
                    </div>
                </div>

                <div class="row" style="margin-bottom: 20px;">
                    <div class="col-md-12">
                        <button type="button" class="btn btn-success" v-on:click="addFeature">Add Feature</button>
                    </div>
                </div>

                <input type="submit" class="btn btn-warning" value="Edit Pricing" />
			</form>
		</div>
	</div>

    <!-- hidden input field to get the ID from URL in javascript -->
	<input type="hidden" id="pricing-id" value="<?php echo $_GET['id']; ?>" />
</div>

This will be almost the same as adding a new pricing table. First, we need to fetch the values from the MySQL database by calling an AJAX request.

<script>
    var editPricingTableApp = new Vue({
        el: "#editPricingTableApp",
        data: {
        	id: "",
            pricing: null
        },
        methods: {

            // [update method goes here]

        	getPricingTable: function () {
        		var self = this;

        		var ajax = new XMLHttpRequest();
        		ajax.open("POST", "get-pricing-table.php", true);

        		ajax.onreadystatechange = function () {
        			if (this.readyState == 4) {
        				if (this.status == 200) {
        					// console.log(this.responseText);
        					var response = JSON.parse(this.responseText);
        					console.log(response);

        					if (response.status == "success") {
        						self.pricing = response.pricing;
        					}
        				}

        				if (this.status == 500) {
        					console.log(this.responseText);
        				}
        			}
        		};

        		var formData = new FormData();
        		formData.append("id", self.id);
        		ajax.send(formData);
        	},
        	deleteFeature: function () {
        		var index = event.target.getAttribute("data-index");
        		this.pricing.features.splice(index, 1);
        	},
        	addFeature: function() {
		        this.pricing.features.push({
		        	value: ""
		        });
		    }
        },
        mounted: function () {
        	this.id = document.getElementById("pricing-id").value;
        	this.getPricingTable();
        }
    });
</script>

Then we need to create a file named “get-pricing-table.php” to fetch the data of this pricing table only.

<?php

// connect with database
$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");

// get pricing table using ID
$sql = "SELECT * FROM pricings WHERE id = :id";
$result = $conn->prepare($sql);
$result->execute([
    ":id" => $_POST["id"]
]);
$pricing = $result->fetch();

// get all the features too of this pricing table
$sql = "SELECT * FROM pricing_features WHERE pricing_id = :pricing_id";
$result = $conn->prepare($sql);
$result->execute([
    ":pricing_id" => $pricing["id"]
]);
$pricing_features = $result->fetchAll();

$pricing["features"] = $pricing_features;

// send the response back to client with the data
echo json_encode([
    "status" => "success",
    "message" => "Pricing table has been fetced.",
    "pricing" => $pricing
]);
exit();

Refresh the edit page now and you will be able to view the data in input fields along with all the features of that pricing table.

Now when this form submits, we need to call an AJAX request to update the data. The following code goes in your [update method goes here] section:

update: function () {
    var self = this;

    var ajax = new XMLHttpRequest();
    ajax.open("POST", "update.php", true);

    ajax.onreadystatechange = function () {
        if (this.readyState == 4) {
            if (this.status == 200) {
                // console.log(this.responseText);
                var response = JSON.parse(this.responseText);
                console.log(response);

                alert(response.message);
            }

            if (this.status == 500) {
                console.log(this.responseText);
            }
        }
    };

    var formData = new FormData();
    formData.append("id", this.id);
    formData.append("title", this.pricing.title);
    formData.append("description", this.pricing.description);
    formData.append("amount", this.pricing.amount);
    formData.append("features", JSON.stringify(this.pricing.features));
    ajax.send(formData);
},

Finally, create a file named “update.php” that will handle this request and will update the pricing table.

<?php

// connect with database
$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");

// udpdate the pricing table
$sql = "UPDATE pricings SET title = :title, description = :description, amount = :amount WHERE id = :id";
$result = $conn->prepare($sql);
$result->execute([
	":title" => $_POST["title"],
	":description" => $_POST["description"],
	":amount" => $_POST["amount"],
	":id" => $_POST["id"]
]);

// delete all the old features
$sql = "DELETE FROM pricing_features WHERE pricing_id = :pricing_id";
$result = $conn->prepare($sql);
$result->execute([
	":pricing_id" => $_POST["id"]
]);

// insert new features
$features = json_decode($_POST["features"]);
foreach ($features as $feature)
{
	$sql = "INSERT INTO pricing_features(pricing_id, value) VALUES (:pricing_id, :value)";
	$result = $conn->prepare($sql);
	$result->execute([
		":pricing_id" => $_POST["id"],
		":value" => $feature->value
	]);
}

// send the response back to client
echo json_encode([
	"status" => "success",
	"message" => "Pricing table has been updated."
]);
exit();

Now refresh the page and try to update any pricing table, you will see it will be changed in the database as well as on the user side. On the user side, you have to refresh the page to see the changes.

Delete

You already have a method named “deletePricing” in your Vue JS instance named “addPricingTableApp“. You just need to modify it as the following:

// method to delete pricing table
deletePricing: function () {
	// get this app instance
	var self = this;

	// get form
	var form = event.target;

	// call an AJAX to delete the pricing table
	var ajax = new XMLHttpRequest();
	ajax.open("POST", "delete.php", true);

	ajax.onreadystatechange = function () {
		if (this.readyState == 4) { // response received
			if (this.status == 200) { // response is successfull
				// console.log(this.responseText);

				// parse the response from JSON string to JS arrays and objects
				var response = JSON.parse(this.responseText);
				console.log(response);

				// remove from local array if deleted from server
				if (response.status == "success") {
					for (var a = 0; a < self.pricings.length; a++) {
						var pricing = self.pricings[a];
						if (pricing.id == form.id.value) {
							self.pricings.splice(a, 1);
							break;
						}
					}
				} else {
					// display an error message
					alert(response.message);
				}
			}

			if (this.status == 500) {
				console.log(this.responseText);
			}
		}
	};

	// append form in form data object
	var formData = new FormData(form);

	// call AJAX with form data
	ajax.send(formData);
},

Now we need to create a file named “delete.php” and in this file, we will simply run the delete query.

<?php

// connect with database
$conn = new PDO("mysql:host=localhost;dbname=test", "root", "");

// create a query to delete the pricing table from database
$sql = "DELETE FROM pricings WHERE id = :id";

// prepare the query
$result = $conn->prepare($sql);

// execute the query
$result->execute([
	":id" => $_POST["id"]
]);

// send the response back to client
echo json_encode([
	"status" => "success",
	"message" => "Pricing table has been deleted."
]);
exit();

Refresh the page now on your admin side and hit the delete button. You will see the row will be deleted from an HTML table and also from the database. If you refresh the user side, it will be removed from there too.

Also learn how to create a dynamic testimonial section on your website.

So that’s how you can create a fully responsive dynamic pricing table on your website, which can be managed from the admin panel. You do not have to go into the code to make changes. If you face any problems in following this, please do let us know in the comments section below.

[wpdm_package id=’1395′]

Instagram feed without API – PHP, MySQL

In this tutorial, we are going to teach you how can you show instagram feed in your PHP website without using any API.

Video tutorial:

First, we need to create a table in our database where we will save all the latest posts from our Instagram page or profile.

<?php

    // connect with database
    $conn = new PDO("mysql:host=localhost:8889;dbname=test", "root", "root");

    // create table to store latest instagram feeds
    $sql = "CREATE TABLE IF NOT EXISTS instagram (
        id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
        url TEXT DEFAULT NULL,
        image_path TEXT DEFAULT NULL,
        video_path TEXT DEFAULT NULL,
        caption TEXT DEFAULT NULL,
        likes TEXT DEFAULT NULL,
        comments TEXT DEFAULT NULL
    )";
    $result = $conn->prepare($sql);
    $result->execute();

    [handle form submission]

?>

[form goes here]

This will create a table named “Instagram” in your database named “test”. The table will have the following columns:

  1. id (primary key)
  2. URL shortcode of Instagram post
  3. Path of the image stored in your server
  4. Path of video stored in your server
  5. Caption (if any) of post
  6. Number of likes on a post
  7. Number of comments on a post

Saving Instagram Feed in MySQL using PHP

Now you need to show a form that will show a link that you need to open in your browser. That link will output the JSON string, you need to copy-paste that JSON string into a textbox in the form.

The following code goes in the [form goes here] section:

<form method="POST" action="index.php">
    <p>
        <a href="https://www.instagram.com/{your_page_name}/?__a=1">Goto this link and paste the JSON string in textbox below</a>
    </p>

    <p>
        <label>Paste JSON</label>
        <textarea name="json" rows="10" cols="30" required></textarea>
    </p>

    <input type="submit" name="submit" class="Save" />
</form>

[show instagram feed]

Refresh the page and you will see a link, a textbox, and a submit button. Click on the link and you will see a JSON string, paste that JSON in the textbox and hit submit. Now we need to write the code to save that JSON in our database.

The following code goes in the [handle form submission] section:

// check if form is submitted
if (isset($_POST["submit"]))
{
    // get JSON from textarea
    $json = $_POST["json"];

    // decode JSON into arrays and objects
    $content = json_decode($json);

    // get all the latest posts
    $edges = $content->graphql->user->edge_owner_to_timeline_media->edges;

    mkdir("instagram-media");

    // delete previous posts from our database
    $sql = "DELETE FROM instagram";
    $result = $conn->prepare($sql);
    $result->execute();

    // loop through all posts
    foreach ($edges as $edge)
    {
        // get single post
        $node = $edge->node;

        // get URL shortcode of post
        $url = $node->shortcode;

        // get caption, if any
        $caption = $node->edge_media_to_caption->edges[0]->node->text;

        // get number of likes
        $likes = $node->edge_liked_by->count;

        // get total number of comments
        $comments = $node->edge_media_to_comment->count;

        // save image in our server if uploaded
        $image_path = "";
        if (!is_null($node->display_url))
        {
            $image_path = "instagram-media/" . $url . ".png";
            file_put_contents($image_path, file_get_contents($node->display_url));
        }

        // save video in our server if uploaded
        $video_path = "";
        if (!is_null($node->video_url))
        {
            $video_path = "instagram-media/" . $url . ".mp4";
            file_put_contents($video_path, file_get_contents($node->video_url));
        }

        // insert in database
        $sql = "INSERT INTO instagram(url, image_path, video_path, caption, likes, comments) VALUES (:url, :image_path, :video_path, :caption, :likes, :comments)";
        $result = $conn->prepare($sql);
        $result->execute([
            ":url" => $url,
            ":image_path" => $image_path,
            ":video_path" => $video_path,
            ":caption" => $caption,
            ":likes" => $likes,
            ":comments" => $comments
        ]);
    }

    echo "<p>Done</p>";
}

[fetch all instagram feeds from database]

This will fetch the latest posts from your instagram page or profile. If there is any image or video attached to it, it will be saved in a folder named “instagram-media”. And it’s path will be stored in the database. If all goes well, it will display a message “Done”.

Fetching Instagram Feed from MySQL via PHP

Now we can easily fetch all latest instagram feeds from our database. Replace the section [fetch all instagram feeds from database] with the following code:

// get all posts from database
$sql = "SELECT * FROM instagram ORDER BY id ASC";
$result = $conn->query($sql);
$instagram_feed = $result->fetchAll();

Finally, we need to show them in our HTML. Replace the section [show instagram feed] with the following code:

<main>
    <div class="container">
        <div class="gallery">

            <!-- loop through all rows from database -->
            <?php foreach ($instagram_feed as $feed): ?>
                <div class="gallery-item" tabindex="0">
                    <!-- wrap with anchor tag, when clicked will go to instagram detail post page -->
                    <a href="https://www.instagram.com/p/<?php echo $feed['url']; ?>" target="_blank" style="color: white;">

                        <!-- thumbnail of post -->
                        <img src="<?php echo $feed['image_path']; ?>" class="gallery-image" />

                        <div class="gallery-item-info">
                            <ul>
                                <!-- show no. of likes -->
                                <li class="gallery-item-likes">
                                    <i class="fa fa-heart"></i>
                                    <?php echo $feed["likes"]; ?>
                                </li>
                                
                                <!-- show no. of comments -->
                                <li class="gallery-item-comments">
                                    <i class="fa fa-comment"></i>
                                    <?php echo $feed["comments"]; ?>
                                </li>
                            </ul>

                            <!-- show caption -->
                            <p><?php echo $feed["caption"]; ?></p>
                        </div>
                    </a>
                </div>
            <?php endforeach; ?>

        </div>
    </div>
</main>

<!-- style CSS -->
<link rel="stylesheet" type="text/css" href="instagram.css?v=<?php echo time(); ?>" />

<!-- font awesome -->
<script src="https://use.fontawesome.com/b8680c3f3d.js"></script>

Comments has been added with each line for explanation. Each post will be wrapped in an anchor tag, so when you clicked on it, you will be redirected to the post’s page on instagram.

Styles

Last thing you are going to need is CSS styles. So create a file named “instagram.css” and paste the following code in it:

/*

All grid code is placed in a 'supports' rule (feature query) at the bottom of the CSS (Line 310). 
        
The 'supports' rule will only run if your browser supports CSS grid.

Flexbox and floats are used as a fallback so that browsers which don't support grid will still recieve a similar layout.

*/

/* Base Styles */

*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    font-family: "Open Sans", Arial, sans-serif;
    min-height: 100vh;
    background-color: #fafafa;
    color: #262626;
    padding-bottom: 3rem;
}

img {
    display: block;
}

.container {
    max-width: 93.5rem;
    margin: 0 auto;
    padding: 0 2rem;
}

.btn {
    display: inline-block;
    font: inherit;
    background: none;
    border: none;
    color: inherit;
    padding: 0;
    cursor: pointer;
}

.btn:focus {
    outline: 0.5rem auto #4d90fe;
}

.visually-hidden {
    position: absolute !important;
    height: 1px;
    width: 1px;
    overflow: hidden;
    clip: rect(1px, 1px, 1px, 1px);
}

/* Profile Section */

.profile {
    padding: 5rem 0;
}

.profile::after {
    content: "";
    display: block;
    clear: both;
}

.profile-image {
    float: left;
    width: calc(33.333% - 1rem);
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 3rem;
}

.profile-image img {
    border-radius: 50%;
}

.profile-user-settings,
.profile-stats,
.profile-bio {
    float: left;
    width: calc(66.666% - 2rem);
}

.profile-user-settings {
    margin-top: 1.1rem;
}

.profile-user-name {
    display: inline-block;
    font-size: 3.2rem;
    font-weight: 300;
}

.profile-edit-btn {
    font-size: 1.4rem;
    line-height: 1.8;
    border: 0.1rem solid #dbdbdb;
    border-radius: 0.3rem;
    padding: 0 2.4rem;
    margin-left: 2rem;
}

.profile-settings-btn {
    font-size: 2rem;
    margin-left: 1rem;
}

.profile-stats {
    margin-top: 2.3rem;
}

.profile-stats li {
    display: inline-block;
    font-size: 1.6rem;
    line-height: 1.5;
    margin-right: 4rem;
    cursor: pointer;
}

.profile-stats li:last-of-type {
    margin-right: 0;
}

.profile-bio {
    font-size: 1.6rem;
    font-weight: 400;
    line-height: 1.5;
    margin-top: 2.3rem;
}

.profile-real-name,
.profile-stat-count,
.profile-edit-btn {
    font-weight: 600;
}

/* Gallery Section */

.gallery {
    display: flex;
    flex-wrap: wrap;
    margin: -1rem -1rem;
    padding-bottom: 3rem;
}

.gallery-item {
    position: relative;
    flex: 1 0 22rem;
    margin: 1rem;
    color: #fff;
    cursor: pointer;
}

.gallery-item:hover .gallery-item-info,
.gallery-item:focus .gallery-item-info {
    display: inline-block;
    justify-content: center;
    align-items: center;
    position: absolute;
    top: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.3);
}
.gallery-item-info ul {
    text-align: center;
    position: relative;
    top: 50%;
    transform: translateY(-50%);
    padding-left: 0px;
    margin-top: 0px;
    margin-bottom: 0px;
}
.gallery-item-info p {
    position: relative;
    top: 50%;
    text-align: center;
    margin-top: 0px;
    padding-left: 10px;
    padding-right: 10px;
}

.gallery-item-info {
    display: none;
}

.gallery-item-info li {
    display: inline-block;
    font-size: 1.7rem;
    font-weight: 600;
}

.gallery-item-likes {
    margin-right: 2.2rem;
}

.gallery-item-type {
    position: absolute;
    top: 1rem;
    right: 1rem;
    font-size: 2.5rem;
    text-shadow: 0.2rem 0.2rem 0.2rem rgba(0, 0, 0, 0.1);
}

.fa-clone,
.fa-comment {
    transform: rotateY(180deg);
}

.gallery-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Media Query */

@media screen and (max-width: 40rem) {
    .profile {
        display: flex;
        flex-wrap: wrap;
        padding: 4rem 0;
    }

    .profile::after {
        display: none;
    }

    .profile-image,
    .profile-user-settings,
    .profile-bio,
    .profile-stats {
        float: none;
        width: auto;
    }

    .profile-image img {
        width: 7.7rem;
    }

    .profile-user-settings {
        flex-basis: calc(100% - 10.7rem);
        display: flex;
        flex-wrap: wrap;
        margin-top: 1rem;
    }

    .profile-user-name {
        font-size: 2.2rem;
    }

    .profile-edit-btn {
        order: 1;
        padding: 0;
        text-align: center;
        margin-top: 1rem;
    }

    .profile-edit-btn {
        margin-left: 0;
    }

    .profile-bio {
        font-size: 1.4rem;
        margin-top: 1.5rem;
    }

    .profile-edit-btn,
    .profile-bio,
    .profile-stats {
        flex-basis: 100%;
    }

    .profile-stats {
        order: 1;
        margin-top: 1.5rem;
    }

    .profile-stats ul {
        display: flex;
        text-align: center;
        padding: 1.2rem 0;
        border-top: 0.1rem solid #dadada;
        border-bottom: 0.1rem solid #dadada;
    }

    .profile-stats li {
        font-size: 1.4rem;
        flex: 1;
        margin: 0;
    }

    .profile-stat-count {
        display: block;
    }
}

/*

The following code will only run if your browser supports CSS grid.

Remove or comment-out the code block below to see how the browser will fall-back to flexbox & floated styling. 

*/

@supports (display: grid) {
    .profile {
        display: grid;
        grid-template-columns: 1fr 2fr;
        grid-template-rows: repeat(3, auto);
        grid-column-gap: 3rem;
        align-items: center;
    }

    .profile-image {
        grid-row: 1 / -1;
    }

    .gallery {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
        grid-gap: 2rem;
    }

    .profile-image,
    .profile-user-settings,
    .profile-stats,
    .profile-bio,
    .gallery-item,
    .gallery {
        width: auto;
        margin: 0;
    }

    @media (max-width: 40rem) {
        .profile {
            grid-template-columns: auto 1fr;
            grid-row-gap: 1.5rem;
        }

        .profile-image {
            grid-row: 1 / 2;
        }

        .profile-user-settings {
            display: grid;
            grid-template-columns: auto 1fr;
            grid-gap: 1rem;
        }

        .profile-edit-btn,
        .profile-stats,
        .profile-bio {
            grid-column: 1 / -1;
        }

        .profile-user-settings,
        .profile-edit-btn,
        .profile-settings-btn,
        .profile-bio,
        .profile-stats {
            margin: 0;
        }
    }
}

If you refresh the page now, you will be able to view all the posts from your database in a grid format. There will be 3 posts in each row. Now you have all the code with the style, you can customize it as per your needs.

So that’s how you can show your Instagram feed without API using simple PHP and MySQL.

Laravel error email notification – PHP

In this article, we are going to show you, how you can receive an email notification when there is an internal server error in your Laravel live site using PHP. Internal Server Error has an error code of 500, which means there is an error in your code. When your website is live, we usually disable the “debug mode” in our “.env” file like this:

APP_DEBUG=false

This prevents displaying the error messages to the user. Because otherwise, it will display your code to the users. But for example, you get a message from one of your users saying that they are seeing an “Internal Server Error” on some page. How would you know what causes the problem for that user ?

Log Laravel error

That is where the “error reporting” comes in. There are many types of error reporting, the most common of them is “logging” which means that you can write the error message in a separate file. But you have to check the file regularly and on daily basis.

But you must know immediately when the error happens. That is why we will be sending an email as soon as there is an error in your code.

Video tutorial:

Handle Laravel error

First, open the file “app/Exceptions/Handler.php” and include the “Mail” and “Auth” libraries in it.

// include Mail to send an email
use Mail;

// include Auth to check if the error happened for an authenticated user
use Auth;

Then change the content of the “register” method to the following:

public function register()
{
    $this->reportable(function (Throwable $e) {
        // get error message
        $error_message = $e->getMessage();

        // get file
        $error_file = $e->getFile();

        // get line number
        $error_line = $e->getLine();

        // get method, GET or POST
        $method = request()->method();

        // get full URL including query string
        $full_url = request()->fullUrl();

        // get route name
        $route = "";

        // get list of all middlewares attached to that route
        $middlewares = "";

        // data with the request
        $inputs = "";
        if (request()->route() != null)
        {
            $route = "uri: " . request()->route()->getName();
            $middlewares = json_encode(request()->route()->gatherMiddleware());
            $inputs = json_encode(request()->all());
        }

        // get IP address of user
        $ip = request()->ip();

        // get user browser or request source
        $user_agent = request()->userAgent();

        // create email body
        $html = $error_message . "\n\n";
        $html .= "File: " . $error_file . "\n\n";
        $html .= "Line: " . $error_line . "\n\n";
        $html .= "Inputs: " . $inputs . "\n\n";
        $html .= "Method: " . $method . "\n\n";
        $html .= "Full URL: " . $full_url . "\n\n";
        $html .= "Route: " . $route . "\n\n";
        $html .= "Middlewares: " . $middlewares . "\n\n";
        $html .= "IP: " . $ip . "\n\n";
        $html .= "User Agent: " . $user_agent . "\n\n";

        // for testing purpose only
        Auth::loginUsingid(1);

        // check if user is logged in
        if (Auth::check())
        {
            // get email of user that faced this error
            $html .= "User: " . Auth::user()->email;
        }

        // subject of email
        $subject = "Internal Server Error";

        // send an email
        Mail::raw($html, function ($message) use ($subject) {
            // developer email
            $message->to("support@adnan-tech.com")
                ->subject($subject);
        });
    });
}

Make sure you have set the SMTP configurations in your “.env” file:

MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=465
MAIL_USERNAME=your_email@gmail.com
MAIL_PASSWORD=your_password
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS=my_email@gmail.com
MAIL_FROM_NAME="${APP_NAME}"

And finally, enable the less secure apps for the above “your_email@gmail.com” account from here.

Enable-less-secure-apps-gmail

You are all set. Now, if you get any error in your live site, you will receive an email notification with details. That’s how you get an email notification whenever you have an error in your Laravel application.

Learn how to enable debugbar in your Laravel application from this tutorial.

10 Javascript Libraries for Every Web Project

In this article, we are going to discuss 10 useful Javascript libraries that you need to use in almost every web project. We will discuss the following libraries along with their use:

  1. Sweetalert
  2. Datetime Picker by XD Soft
  3. Socket IO
  4. DataTable
  5. RichText
  6. PDFObject
  7. Vue JS
  8. Notify JS
  9. Chart JS
  10. TimeCircles

1. Sweetalert

Sweetalert is a JS library used to display alert messages. You can also use it for displaying confirm messages i.e. ask for the user’s confirmation before performing any action. To display a simple alert message:

swal("Message goes here");

To display an alert with a title:

swal("My Title", "Text goes here");

Success alert:

swal("Done", "Your message goes here", "success");

Error alert:

swal("Error", "Error message goes here", "error");

For confirmation dialog:

swal({
	title: "Confirm",
	text: "Are you sure, this data will be removed ?",
	icon: "warning",
	buttons: true,
	dangerMode: true,
})
.then((isOkay) => {
	if (isOkay) {
		//
	}
});

Learn more about how to use the confirmation dialog properly from here. You can also use it as a prompt:

swal("Enter your name:", {
    content: "input",
})
.then((value) => {
    if (value == "") {
        return false;
    }
    console.log(value);
});

You can read more about it from their official page.

2. Datetime Picker by XD Soft

It is a JS library that is used to display a calendar with a date and time picker. You can use it to get the user’s date of birth during registration. The basic usage is, you need to give your input field a unique ID:

<input type="text" id="datetimepicker" />

Then you can display the datetimepicker by calling the function:

jQuery("#datetimepicker").datetimepicker();

Make sure you have jQuery included in your project, this library requires jQuery. You can see this library in action from here. The complete documentation of this library is found on their official page.

3. Socket IO

Socket IO is used for real-time communication. If you are working on a feature that requires data from the server after regular intervals, there are 2 options to achieve this:

  1. Polling
  2. Sockets

Polling

This technique requires calling an AJAX request to the server every few seconds. This will overload the server with a lot of requests. There will be a lot of unnecessary requests to the server and if you have a lot of users, then your website might get crashed.

Sockets

The solution to the above problem is the sockets. Sockets attach a listener to the client-side and emit events from the server. The client is continuously listening to that event, whenever that event is received, the client will perform the action accordingly.

The difference between Polling and Sockets can be explained by this example:

For example, you are going on a trip with your family. Your Dad is driving the car and you are in the back seat. And you are continuously asking your Dad “did we reached the destination ?” every few minutes. This is Polling.

And in the other case, you ask your Dad “please inform me when we reach our destination”. Now you are silently listening to your Dad, who will send an event to you only when you reached your destination. This is Socket.

Learn how to use sockets with Node JS. Complete documentation can be found on their official page.

4. DataTable

This library is used to display data in tabular form. It has a built-in search feature, that allows you to search the entire table. You can search value from any row from any column. The search is case insensitive so you do not have to worry about capital or small letters.

It also has a sorting feature that allows you to sort the data based on any column. It also allows you to decide how many records you want to show on one page (e.g. 25, 50, 75, or 100). It has built-in pagination based on the number of records you are displaying on one page.

Its usage is rather simple. You just need to give your table tag a unique ID:

<table id="table"></table>

Then in your Javascript, simply initialize the library:

$("#table").DataTable();

It also requires jQuery to be included in your project. You can see this library in action from here. You can learn more about it from their official page.

5. RichText

This library applies to your textarea tag. It makes your text area as WYSIWYG (What You See Is What You Get). It enhances the Textarea field and gives it more options like:

  1. Text formatiing (font size, bold, italic, underline, color).
  2. Text alignment (left, center, right, justify).
  3. Headings & paragraphs.
  4. Upload images.
  5. Attach URLs.
  6. Adding tables.
  7. Or you can even add HTML tags too.

Its usage is almost the same as the DataTable library. You need to give your textarea a unique ID:

<textarea id="content" name="content"></textarea>

And in your Javascript, you need to initialize it like this:

$("#content").richText();

This library also requires jQuery to be included. Full documentation of this library can be found here.

6. PDFObject

When working with PDF files, if you want to show PDF files from your website, you have 3 options:

  1. Make the PDF file downloadable, so user can view it offline too.
  2. Display the PDF in a new window or tab.
  3. Embed the PDF in your website specific section.

This library fulfills the 3rd approach. It embeds the PDF file in your website using a div tag. Place a div tag anywhere in your code where you want to show the PDF file:

<div id="pdf"></div>

Then in your Javascript, initialize the library with a URL of the PDf file:

PDFObject.embed("my-file.pdf", "#pdf");

More information can be found on pdfobject.com.

7. Vue JS

Vue JS is more a framework than a library. But since it can be integrated with any project whether it is in core PHP, Laravel, WordPress, or Node JS, it can be called a library. You can learn all functions of it from their official guide. You can check our following projects developed in Vue JS:

  1. Picture Competition Web App – Vue JS, Node JS, Mongo DB
  2. Firebase Storage – Vue JS
  3. Financial Ledger Web App – Vue JS, Node JS, Mongo DB

You will learn a lot about Vue JS from the above projects.

8. Notify JS

This lightweight library is used to display notification pop-ups on the corners of the browser window. This library also requires jQuery to be included before using it. Its usage is extremely easy, you simply need to call the following function when you want to show the notification:

$.notify("New message");

By default, it displays a pop-up on the top right, but you can decide the position of it. You can learn more about this library from here.

9. Chart JS

As the name suggests, this library is used to draw charts and graphs on your website. You can show statistics to the users using this library. It uses canvas to render charts. The following code will create a simple bar chart:

<canvas id="myChart" width="400" height="400"></canvas>
<script>
var ctx = document.getElementById('myChart').getContext('2d');
var myChart = new Chart(ctx, {
    type: 'bar',
    data: {
        labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
        datasets: [{
            label: '# of Votes',
            data: [12, 19, 3, 5, 2, 3],
            backgroundColor: [
                'rgba(255, 99, 132, 0.2)',
                'rgba(54, 162, 235, 0.2)',
                'rgba(255, 206, 86, 0.2)',
                'rgba(75, 192, 192, 0.2)',
                'rgba(153, 102, 255, 0.2)',
                'rgba(255, 159, 64, 0.2)'
            ],
            borderColor: [
                'rgba(255, 99, 132, 1)',
                'rgba(54, 162, 235, 1)',
                'rgba(255, 206, 86, 1)',
                'rgba(75, 192, 192, 1)',
                'rgba(153, 102, 255, 1)',
                'rgba(255, 159, 64, 1)'
            ],
            borderWidth: 1
        }]
    },
    options: {
        scales: {
            y: {
                beginAtZero: true
            }
        }
    }
});
</script>

There are many graphs & charts provided by this library. You can learn the implementation of all chars from their official page.

10. TimeCircles

This is used to create a countdown timer. You can set the date and the timer will start counting down till that time. This also requires jQuery on your website. Its usage is simple, you just need to give a data-date attribute to the div tag where you want to show the countdown timer. data-date attribute’s value will be the date and time when the timer hits zero. Give it a unique ID so it can be accessible in Javascript.

<div data-date="2021-10-12 23:02:55" id="count-down"></div>

Then in your Javascript, initialize the countdown timer with the following function:

$("#count-down").TimeCircles();

Learn How to Create a Dynamic Countdown Timer. Download the library from here.

What’s Next

You should learn all the libraries above and create small programs in them. But then you need to create a web application where you should apply all the above libraries together. We are going to give you a simple task that helps you learn how to combine all libraries together to create great web applications.

Assignment

Create a simple website that inputs date using DateTimePicker, description using RichText, an input field to upload PDF files and a submit button. When the submit button is pressed, it should show a message using Sweetalert that the message has been sent. It should send an event to the server using Socket IO and the other user should receive that event and display the description in a table using DataTable.

Another user should also see a notification using NotifyJS that a new event has been received. You need to show a countdown timer using TimeCircles till the date field value ends. You should also display the uploaded PDF file in a div tag using the PDFObject library.

Complete assignment’s frontend should be done in Vue JS. If you face any problem in doing this assignment, kindly do let us know in the comments section below.

Picture Competition Web App – Node JS, Mongo DB, Vue JS

Picture Competition is a mobile responsive real-time web application developed in Node JS and Mongo DB. Its frontend is designed in Bootstrap and Vue JS. You can create competition between 2 people and the others can vote on the person based on looks or skills etc.

Demo:

https://github.com/adnanafzal565/picture-competition-web-app-node-js

FeaturesFreePremium $100
Login and registrationYesYes
Create competitionsYesYes
Search & sortYesYes
Vote on CompetitionYesYes
Delete CompetitionYesYes
Reset PasswordNoYes
Email VerificationNoYes
Adult Image ValidationNoYes
SMTP Configurations Admin PanelNoYes
Real-time VotesNoYes
Real-time Admin Panel StatisticsNoYes
Real-time CommentsNoYes
User ProfileNoYes
Realtime update via socketsNoYes
NotificationsNoYes
Load more buttonNoYes
Admin panelNoYes
Manage competitionsNoYes
Free customer supportNoYes

Mongo DB Backend

competition’s collection

1. Login and Registration

It uses login authentication using JWT (JsonWebToken). It does not use the browser session due to the following reasons:

  1. Sessions are destroyed once your app is restarted from terminal.
  2. Sessions are not linked with the server, they are stored in browser only.

The benefit of using JWT is that you can always log out a user from your Mongo DB. Just go to the “users” collection and find the specific user. Then empty the “accessToken” field of that user’s document.

Login and Registration

2. Create Competitions

Registered users can create competitions between 2 users. You can enter name and upload 1 picture of each competitor. There is no limit in the number of competitions to create, you can create as many as you want. You can view your created competitions on your “My Competitions” page.

As soon as you create a competition, it will automatically be displayed to all the users as well as to the admin in real-time. Users do not have to refresh the page to see new competitions. It uses Socket IO to display data in real-time.

Create Competition

3. Search

Users can search competitions by the name of competitors. Data is filtered and rendered using Vue JS. There is no need to press the enter key, the data is filtered as soon as the user starts typing in the search box.

Search and Sort

4. Sort

Users can also sort the competitions by any of the following for sorting functions:

  1. Highest votes to lowest.
  2. Lowest votes to highest.
  3. Oldest to newest.
  4. Newest to oldest (default).

5. Vote on Competition

Logged-in users can vote on the competition. You can either vote on one of the competitors. Once the vote is cast on one competitor, it cannot be removed. Also, you can cast your vote on only one of the competitors, not on both of them. It is also real-time, as soon as the vote is cast, it will automatically be displayed to all the users and the counter is incremented. The counter displays the number of votes cast on each competitor.

Votes

6. Delete Competition

Competitions can only be deleted by either of the users who created the competition, or by the admin. Once the competition is deleted, all the uploaded images will be deleted too. As soon as the competition is deleted, it will automatically be removed from all the other users too, without having them refresh the page.

7. Realtime Update using Sockets

Sockets are used for real-time communication. Instead of fetching the data from the server after regular intervals, sockets attach listeners to the client-side. Listeners are listening to the events sent from the server. The server will emit the event and the client will listen to that event and respond accordingly. In this project, sockets are used for the following features:

  1. When competition is created.
  2. When competition is deleted.
  3. To increase the counter after vote is casted to the competition.
  4. Notifications.

8. Notifications

When a competition is deleted by the admin, the admin will write the reason for the deletion. Thus, a notification will be sent to the user along with the reason why his competition was removed. By default, notification status is “unread” and they are highlighted. As soon as the user clicks on any of the notifications, that notification will be marked as “read” and it will no longer be highlighted.

Notifications

9. Load More Button

When the data in the database increases, it is not feasible to load all the data in a single AJAX request. So a “load more” button is created to solve this problem. For example, 10 records are fetched in the first AJAX request. The next 10 records will be fetched when the “load more” button is clicked, and so on.

Load More

10. Admin Panel

Admin panel is created so you (administrator) can delete any competition you find offensive. The default email and password of admin are:

email = admin@gmail.com
password = admin

11. Manage Competitions

Admin can delete competitions that he finds offensive. However, the admin must give the reason why that competition is deleted. A notification will be sent to the user who created that competition and he will be able to view it from the top navigation bar.

12. Reset Password

Now you will be able to reset your password if you ever forgot. You just need to enter your email address and an email will be sent to you with a link to reset the password. We are using the nodemailer module to send an email.

Forgot Password
Reset Password

13. Email Verification

When a new user registers, we are sending a verification email to the user’s entered email address. The user will not be able to log in until he verifies his email address. When a user clicks the link on his email address, he will receive a message that says that his account is verified. Then he will be able to log in successfully.

Email Verification

14. SMTP Configurations from Admin Panel

To send an email, you will need an SMTP server. Every SMTP server requires some configurations to set up that include, host, port, email, and password. You can write these values directly hardcoded in your code, but to update these values in the future, you have to find these values in the code and update them.

In this project, you can set these configurations directly from the admin panel. Once the values are set, new emails will be sent using the new configurations.

SMTP configurations from admin panel

15. Adult Image Validation

This is a must-have feature if you are creating a website that allows users to upload pictures and they will be seen to the world. Anyone can upload an image that contains adult content, and it will not be good for your business. So when the user is uploading pictures while creating competition, the system will automatically check if the image is safe to upload.

If the image is an adult image, then an error will be shown to the user and it will not be uploaded.

16. Admin Panel Stats

Admin can see total users, total competitions, and total votes cast so far. They are also real-time, so when a new user is registered, or new competition is created, or event a new vote is cast, it will automatically be incremented here.

Also, when competition is deleted, the number will automatically get decremented as well, without having the admin refresh the page.

Admin Panel Stats

17. Real-time Comments

Users can comment on each competition. And they are also real-time as well. Once a new comment is added, it will immediately be displayed to all the other users as well. They do not have to refresh the page to see new comments.

Real-time Comments

18. User Profile

Users can now update their names and profile pictures. We are using the fs (file system) module to upload the picture. User can also add their bio, date of birth, country, and social media links. Media links include Facebook, Instagram, google plus, Twitter, and LinkedIn.

User can also change their account password. In order to change the password, the user must enter the current password. The new password should be entered twice for confirmation.

User Profile

19. Free Customer Support

This is not a feature of the project, but it is a free service provided for the pro version only. That means if you find any difficulty in installing or configuring the project, we will help you install it. Also, if you encounter any error or a bug in the released version, then it can be fixed too.

These are all the features we have right now in the picture competition web app. We are open to more ideas. If you have more ideas to add, kindly do let us know.

Share a Draft, Getting Only 5 Posts [FIXED] – WordPress

In this tutorial, we are going to teach you, how you can display all posts in your Share a Draft plugin in WordPress. This plugin is used to share your unpublished posts using a link. In WordPress, by default only published posts are shared. You can download the plugin manually from here.

Video tutorial:

You just need to follow the steps below:

  1. From your WordPress admin panel, go to “Plugins > Plugin Editor”.
  2. Select plugin to edit “Share a Draft”.
  3. Select plugin file “shareadraft.php”.
  4. Search for 2 variables; “$my_unpublished” and “$others_unpublished”.
  5. Add the “nopaging” value in the “get_posts” function of both of these arrays.
$my_unpublished = get_posts( array(
	'post_status' => $unpublished_statuses,
	'author' => $current_user->ID,
	// some environments, like WordPress.com hook on those filters
	// for an extra caching layer
	'suppress_filters' => false,
	"nopaging" => true
) );
$others_unpublished = get_posts( array(
	'post_status' => $unpublished_statuses,
	'author' => -$current_user->ID,
	'suppress_filters' => false,
	'perm' => 'editable',
	"nopaging" => true
) );
  1. Click the “Update File” button and refresh your plugin page.

Then you will be able to view all of your drafted posts.

Learn how to fix your WP Carousel Slider plugin height issue following this tutorial.

Firebase Storage – Upload, Download, and Delete

In this tutorial, we are going to teach you, how you can upload, download and delete files from Firebase Storage. Firebase Storage is a service provided by Google that allows you to save files on their server. The free plan allows you to upload data up to 1 GB. More data can be bought from the Firebase Pricing page.

What you are going to learn:

  1. Upload files on Firebase Storage.
  2. Save the data in Realtime Database.
  3. Fetch files from the Firebase Storage.
  4. Download files from Firebase Storage.
  5. Fix CORS issue if working in localhost.
  6. Install gsutil.
  7. Delete files from Firebase Storage.

Video tutorial:

Upload file

We are going to upload the file to the Firebase storage and save its path in Firebase Realtime Database. It is another service provided by Google to save data in JSON format. To upload the file, we are going to show a form with an input type file that allows the user to select any type of file. We will be using Vue JS to render the HTML to view the data in real-time, for example, newly uploaded files will be displayed automatically and deleted files will be removed from the view without having to refresh the page.

<div id="app">
	<form id="upload-form">
		<input type="file" name="file" required />
		<input type="submit" value="Upload" />
	</form>

    [show all uploaded here]
</div>

<script src="vue.min.js"></script>

This will display a form with an input type file to upload a file. And a submit button which when clicked will submit the form. Vue JS production file can be downloaded from here. Now you need to attach a submit event listener to this form using Javascript.

<script type="module">

	// [initialize firebase here]

	window.addEventListener("load", function () {
		document.getElementById("upload-form").addEventListener("submit", function () {
			event.preventDefault();
			var form = event.target;

			var file = form.file.files[0];
			console.log(file);
			// [upload in storage here]
		});
	});
</script>

If you select the file and hit submit, you will see your selected file object in the browser console. Now you need to do 2 things; initialize firebase, and upload the file. To initialize firebase, you need to create an app at Firebase Console.

  1. Add new project.
  2. Enter name of project.
  3. When project is created, select “web”.
  4. Copy the firebaseConfig variable.

Replace the [initialize firebase here] section with the following:

import { initializeApp } from "https://www.gstatic.com/firebasejs/9.1.0/firebase-app.js";

import { getStorage, ref as stRef, uploadBytes } from "https://www.gstatic.com/firebasejs/9.1.0/firebase-storage.js";

import { getDatabase, ref as dbRef, push, set } from "https://www.gstatic.com/firebasejs/9.1.0/firebase-database.js";

// Your web app's Firebase configuration
const firebaseConfig = {
	apiKey: "",
	authDomain: "",
	projectId: "",
	storageBucket: "",
	messagingSenderId: "",
	appId: ""
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

// Get a reference to the storage service, which is used to create references in your storage bucket
const storage = getStorage(app);

const database = getDatabase();
const databaseReference = dbRef(database, "files");

Replace the firebaseConfig variable with your firebase configurations. Now replace the [upload in storage here] section with the following:

const storageRef = stRef(storage, "files/" + file.name);

uploadBytes(storageRef, file).then(function (snapshot) {
	var newFileRef = push(databaseReference);

	set(newFileRef, {
		"name": file.name
	});
});

This will upload the file in Firebase Storage in a folder named “files”. You can see it in your project’s dashboard in left menu in “Storage” page. Also, it will create a new element in “files” array in “Realtime Database” page.

Displaying Uploaded Files in Firebase Storage

Now the file is being uploaded in Firebase Storage and it’s path is also being saved in realtime database. Now you need to show all uploaded files. Replace the [show all uploaded here] section with the following:

<table>
	<tr>
		<th>ID</th>
		<th>Name</th>
		<th>File</th>
		<th>Actions</th>
	</tr>

	<tr v-for="file in files">
		<td>{{ file.id }}</td>
		<td>{{ file.name }}</td>
		<td>
			[download link here]
		</td>
		<td>
			[delete button here]
		</td>
	</tr>
</table>

This will create a simple HTML table. Now you need to do the following things:

  1. Initialize Vue JS.
  2. Get all data from realtime database.
  3. Render in HTML table.

You can initialize the Vue JS from the following code:

var vueApp = new Vue({
	el: "#app",
	data: {
		files: []
	},
    // [updated event goes here]
});

Then include “onValue” in your “firebase-database” import. So your firebase database import line will become:

import { getDatabase, ref as dbRef, push, set, onValue } from "https://www.gstatic.com/firebasejs/9.1.0/firebase-database.js";

Note the “onValue” in the import block. Similarly, add the “getDownloadURL” in the import of “firebase-storage”. So your firebase storage import line will become:

import { getStorage, ref as stRef, uploadBytes, getDownloadURL } from "https://www.gstatic.com/firebasejs/9.1.0/firebase-storage.js";

Note the “getDownloadURL” in the import block. Then write the following code in your Javascript:

onValue(databaseReference, function (snapshot) {
	snapshot.forEach(function (childSnapshot) {
		const value = childSnapshot.val();

		const storageRefDownload = stRef(storage, "files/" + value.name);
		getDownloadURL(storageRefDownload).then(function (url) {
			vueApp.files.push({
				"id": childSnapshot.key,
				"name": value.name,
				"url": url
			});
		});
	});
});

You can apply the following CSS styles to make the table look a little nice:

<style>
	table, th, td {
		border: 1px solid black;
		border-collapse: collapse;
	}
	th, td {
		padding: 25px;
	}
</style>

Download File from Firebase Storage

Now you need to download the file. First, you need to replace the [download link here] section with the following:

<a v-bind:href="file.url" v-bind:download="file.name" onclick="downloadFile();">Download</a>

Create the following function in your Javascript:

function downloadFile() {
	// prevent default href action
	event.preventDefault();

	// get URL from href
	var anchor = event.target;
	var url = anchor.getAttribute("href");

	// get blob data
	const xhr = new XMLHttpRequest();
	xhr.responseType = "blob";
	xhr.onload = function (event) {
		const blob = xhr.response;

		// get clickable URL of blob data
		const blogUrl = window.URL.createObjectURL(blob);

		// replace href with new blob value
		anchor.setAttribute("href", blogUrl);

		// remove the onclick listener
		anchor.removeAttribute("onclick");

		// download the file
		anchor.click();

		// free up the memory
		window.URL.revokeObjectURL(blogUrl);
	};
	xhr.open("GET", url);
	xhr.send();
}

// make the function global so it can be accessible from anchor tag onclick
window.downloadFile = downloadFile;

Run the code now, and you will be able to download the file.

Note: If you get a CORS error while working in localhost, then do the following steps:

  1. Download gsutil from here.
  2. Extract the ZIP and paste the folder to a permanent location in your system.
  3. Open the terminal or command prompt in that extracted folder and run the following command in it:
./install.sh
  1. Once installed, run the following command to initialize it:
gcloud init
  1. It will ask for your project name too.
  2. After the project is set and gcloud is initialized, create a file named “cors.json” at the root of your project.
  3. Your “cors.json” must have the following content:
[
	{
		"origin": ["*"],
		"method": ["GET"],
		"maxAgeSeconds": 3600
	}
]
  1. Then run the following command in your terminal:
gsutil cors set cors.json gs://<your-cloud-storage-bucket>
  1. Now try to download the file again, your CORS error should be fixed.

Delete File from Firebase Storage

First, replace the [delete button here] section with the following:

<form class="delete-form">
	<input type="hidden" name="id" v-bind:value="file.id" />
	<input type="hidden" name="name" v-bind:value="file.name" />
	<input type="submit" value="Delete" />
</form>

Then add “deleteObject” in your import of “firebase-storage”. So your import line will look like this:

import { getStorage, ref as stRef, uploadBytes, getDownloadURL, deleteObject } from "https://www.gstatic.com/firebasejs/9.1.0/firebase-storage.js";

Then add the following event in your [updated event goes here] section:

updated: function () {
	var forms = document.querySelectorAll(".delete-form");
    for (var a = 0; a < forms.length; a++) {
    	forms[a].setAttribute("onsubmit", "onDeleteFormSubmit();");
    }
}

This will attach an event listener to every delete form, which will be called when the form is submitted. Now you need to create the following function in your Javascript:

function onDeleteFormSubmit() {
	event.preventDefault();
	var form = event.target;

	const tempDbRef = dbRef(database, "files/" + form.id.value);
	set(tempDbRef, null);

	const deleteStorageReference = stRef(storage, "files/" + form.name.value);
	deleteObject(deleteStorageReference);

	for (var a = 0; a < vueApp.files.length; a++) {
		if (vueApp.files[a].id == form.id.value) {
			vueApp.files.splice(a, 1);
			break;
		}
	}
}
window.onDeleteFormSubmit = onDeleteFormSubmit;

This will first delete the data from the real-time database. Then it will delete the file from Firebase Storage. Finally, it will remove it from the Vue JS array, so it will automatically be removed from the HTML table too.

You can also create a realtime chat in Firebase. Learn how to do it from here.

Now you will be able to upload files to the Firebase Storage and save its path in Realtime Database. And also to download and delete the files from it. If you face any problems facing this tutorial, kindly let us know in the comments section below.