Телефонный справочник

Добавить контакт
Редактировать контакт
X
Вы уверены удалить контакт?
X
Новый контакт
X
Добавить контакт
Фронтенд был написан на нативном JavaScript. Для сетевых запросов использовалась технология AJAX, современный и мощный метод fetch(). Создание нового контакта, обновление и удаление происходят без перезагрузки страницы.
Бэкенд написан на РНР, база данных MySQL. Простая валидация входных данных происходит как на клиенте, так и на сервере.
Максимальное количество контактов которое можно записать ограничено до 20.
Данные с контактами обновляются каждые 20 минут. Все изменённые или добавленные записи будут удалены и загрузятся данные по умолчанию.
Файловая структура
// api/create.php
<?php
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
include_once '../config/database.php';
include_once '../class/contacts.php';
include_once '../class/count.php';
$database = new Database();
$db = $database->getConnection();
// count of records in DB, maximum: 20
$countOfRows = new Count( $db );
$quantity = $countOfRows->getCount();
// create new contact
$contact = new Contact( $db );
// ======== comments block ============
// file_get_contents() - Читает содержимое файла в строку
// json_decode() - Принимает закодированную в JSON строку и преобразует её в переменную PHP.
// php://input - передаётся через POST в Body(raw, JSON) Postman
// ======== comments block ============
$data = json_decode(file_get_contents( "php://input" ));
// Limitation of records in DB
// if ( $quantity < 20 ) {
if ( $quantity < 21 ) {
//contact values
if ( !empty($_POST) ) { // request FORM submit
$contact->fio = $_POST['fio'];
$contact->phone = $_POST['phone'];
$contact->job = $_POST['job'];
} else { // request JSON
$contact->fio = $data->fio;
$contact->phone = $data->phone;
$contact->job = $data->job;
}
if( $contact->createContact() ) {
http_response_code( 200 );
echo json_encode('Новый контакт создан.');
} else {
echo json_encode('Новый контакт не создан.');
}
} else {
echo json_encode('Ограничение в количестве контактов.');
}
?>
// api/read.php
<?php
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
include_once '../config/database.php';
include_once '../class/contacts.php';
$database = new Database();
$db = $database->getConnection();
$contacts = new Contact( $db );
$stmt = $contacts->getContacts();
$itemCount = $stmt->rowCount();
if ( $itemCount > 0 ){
$contactArr = array();
while ( $row = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
extract( $row );
$e = array(
"id" => $id,
"fio" => $fio,
"phone" => $phone,
"job" => $job
);
array_push( $contactArr, $e );
}
http_response_code( 200 );
echo json_encode( $contactArr );
} else {
http_response_code( 404 );
echo json_encode(
array("message" => "Контакт не был найден.")
);
}
?>
// api/update.php
<?php
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
include_once '../config/database.php';
include_once '../class/contacts.php';
$database = new Database();
$db = $database->getConnection();
$contact = new Contact( $db );
$data = json_decode( file_get_contents( "php://input" ) );
//contact values
if ( !empty( $_POST ) ) { // request FORM submit
$contact->id = $_POST['id'];
$contact->fio = $_POST['fio'];
$contact->phone = $_POST['phone'];
$contact->job = $_POST['job'];
} else { // request JSON
$contact->id = $data->id;
$contact->fio = $data->fio;
$contact->phone = $data->phone;
$contact->job = $data->job;
}
if( $contact->updateContact() ) {
echo json_encode( "Контакт был обновлён." );
} else {
echo json_encode( "Контакт не обновлён." );
}
?>
// api/delete.php
<?php
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
include_once '../config/database.php';
include_once '../class/contacts.php';
$database = new Database();
$db = $database->getConnection();
$contact = new Contact($db);
$data = json_decode( file_get_contents( "php://input" ) );
//contact values
if ( !empty($_POST ) ) { // request FORM submit
$contact->id = $_POST['id'];
} else { // request JSON
$contact->id = $data->id;
}
if( $contact->deleteContact() ) {
echo json_encode( "Контакт был удалён." );
} else {
echo json_encode( "Контакт не был удалён" );
}
?>
// config/database.php
<?php
class Database {
private $host = "localhost";
private $database_name = "database_name";
private $username = "username";
private $password = "password";
public $conn;
public function getConnection(){
$this->conn = null;
try{
$this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->database_name, $this->username, $this->password);
$this->conn->exec("set names utf8");
}catch(PDOException $exception){
echo "Database could not be connected: " . $exception->getMessage();
}
return $this->conn;
}
}
?>
// class/contacts.php
<?php
class Contact {
// Connection
private $conn;
// Table
private $db_table = "phonebook";
// Columns
public $id;
public $fio;
public $phone;
public $job;
// Db connection
public function __construct( $db ){
$this->conn = $db;
}
// GET ALL
public function getContacts() {
$sqlQuery = "SELECT `id`, `fio`, `phone`, `job` FROM " . $this->db_table . "";
$stmt = $this->conn->prepare( $sqlQuery );
$stmt->execute();
return $stmt;
}
// CREATE
public function createContact() {
$sqlQuery = "INSERT INTO
". $this->db_table ."
SET
fio = :fio,
phone = :phone,
job = :job";
$stmt = $this->conn->prepare( $sqlQuery );
// sanitize
$this->fio = htmlspecialchars(strip_tags( $this->fio ));
$this->phone = htmlspecialchars(strip_tags( $this->phone ));
$this->job = htmlspecialchars(strip_tags( $this->job ));
// bind data
$stmt->bindParam( ":fio", $this->fio );
$stmt->bindParam( ":phone", $this->phone );
$stmt->bindParam( ":job", $this->job );
if( $stmt->execute() ){
return true;
}
return false;
}
// UPDATE
public function updateContact() {
$sqlQuery = "
UPDATE
". $this->db_table ."
SET
fio = :fio,
phone = :phone,
job = :job
WHERE
id = :id";
$stmt = $this->conn->prepare( $sqlQuery );
$this->fio = htmlspecialchars(strip_tags( $this->fio ));
$this->phone = htmlspecialchars(strip_tags( $this->phone ));
$this->job = htmlspecialchars(strip_tags( $this->job ));
$this->id = htmlspecialchars(strip_tags( $this->id ));
// bind data
$stmt->bindParam(":fio", $this->fio);
$stmt->bindParam(":phone", $this->phone);
$stmt->bindParam(":job", $this->job);
$stmt->bindParam(":id", $this->id);
if( $stmt->execute() ) {
return true;
}
return false;
}
// DELETE
function deleteContact() {
$sqlQuery = "DELETE FROM " . $this->db_table . " WHERE id = ?";
$stmt = $this->conn->prepare( $sqlQuery );
// $this->id=htmlspecialchars(strip_tags( $this->id) );
$this->id = intval( $this->id ) ;
$stmt->bindParam(1, $this->id);
if( $stmt->execute() ){
return true;
}
return false;
}
}
?>
// class/count.php
<?php
class Count {
private $conn;
public $count;
// Db connection
public function __construct($db){
// connection
$this->conn = $db;
}
/**
* return NUMBER quantity of DB rows
*/
public function getCount(){
$this->count = $this->conn->query('SELECT id FROM phonebook')->fetchAll(PDO::FETCH_ASSOC);
return count($this->count);
}
}
?>
// Start rendering rows of contacts
const urlRead = 'api/read.php'
const row = document.querySelector(".top")
async function render() {
// Default options are marked with *
const response = await fetch(urlRead, {
method: 'GET', // .*GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // .*default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *client
//body: JSON.stringify(data) // body data type must match "Content-Type" header
});
let json = await response.json()
for ( let i = 0; i < json.length; i++) {
let el = document.createElement('div');
el.classList.add('row');
el.innerHTML = `ЗДЕСЬ РАЗМЕЩЕНЫ HTML ТЭГИ С DIV, P... ФОРМИРУЮЩИЕ строку .row
ОНИ ПЛОХО ВЫВОДЯТСЯ, ПОЭТОМУ ВМЕСТО ТЭГОВ ПИШУ ТЕКСТОМ`;
// attach just rendered row to div class="top"
demo.append(el);
}
//! run function after that click on IMG are "visible" to handling
initial()
}
render();
//! after rendering new ROWs from DB, clicks by images are stop working
//! to fix that making "refresh" variable imgClick for JS
function initial() {
const imgClick = document.querySelectorAll('.rowRight img')
for (let i = 0; i < imgClick.length; i++) {
imgClick[i].addEventListener("click", function() {
// handler img edit or delete
if ( imgClick[i].alt == "edit") {
// set values from image attributes to Modal Edit inputs
modalEditId = imgClick[i].dataset.id
modalEditFio.value = imgClick[i].dataset.fio
modalEditPhone.value = imgClick[i].dataset.phone
modalEditJob.value = imgClick[i].dataset.job
// hide scroll bar body
body.style.overflow = "hidden"
// turn Modal Edit display:block
// modalEdit.style.display = "flex"
modalEdit.style.display = "flex"
} else {
// hide scroll bar body
body.style.overflow = "hidden"
// so click by image is having attribut "alt"= "delete"
modalDelete.style.display = "flex" // show modal DELETE
// set data-delete with value from data-delete from img(alt="delete")
btnDelete.dataset.delete = imgClick[i].dataset.delete
}
});
}
}
// Variables
const body = document.body;
var modalEditId;
const modalEditFio = document.getElementById('modalEditFio')
const modalEditPhone = document.getElementById('modalEditPhone')
const modalEditJob = document.getElementById('modalEditJob')
const btnEdit = document.getElementById('btnEdit')
// getting previous siblings class "loader"
const loaderEdit = btnEdit.previousElementSibling
const modalEdit = document.getElementById('modal-edit')
const editResult = document.getElementById('edit-result')
const btnAddContact = document.getElementById('addContact')
const btnCreate = document.getElementById('btnCreate')
// getting previous siblings class "loader"
const loaderCreate = btnCreate.previousElementSibling
const modalCreate = document.getElementById('modal-create')
const createResult = document.getElementById('create-result')
const btnDelete = document.getElementById('btnDelete')
const btnEscape = document.getElementById('btnEscape')
// getting previous siblings class "loader"
const loaderDelete = btnDelete.previousElementSibling
const modalDelete = document.getElementById('modal-delete')
const deleteResult = document.getElementById('delete-result')
const refreshDb = document.getElementById('refreshDb')
// Navigation variables
const demo = document.getElementById('demo')
const task = document.getElementById('task')
const solution = document.getElementById('solution')
const code = document.getElementById('code')
const closeModal = document.querySelectorAll('.close-modal');
// click on X in modal engage closing ALL modals
for (let i = 0; i < closeModal.length; i++) {
closeModal[i].addEventListener("click", function() {
modalEdit.style.display = "none";
modalCreate.style.display = "none";
modalDelete.style.display = "none";
// change style.display of loaders
loaderCreate.style.display = "none"
loaderEdit.style.display = "none"
loaderDelete.style.display = "none"
// clear innerHTML results of sendings
createResult.innerHTML = ""
editResult.innerHTML = ""
deleteResult.innerHTML = ""
// show scroll bar body
body.style.overflow = "auto"
});
}
//! after rendering ROWs from DB clicks on IMG stops working
//! to fix that adding "document.addEventListener"
// add new contact
btnAddContact.addEventListener("click", function() {
//clear all fields in modalCreate
modalCreateFio.value = ""
modalCreatePhone.value = ""
modalCreateJob.value = ""
// remove scroll bar body
// open modalCreate
body.style.overflow = "hidden"
modalCreate.style.display = "flex"
})
// CREATE handler
btnCreate.addEventListener("click", function() {
let a = modalCreateFio.value
let b = modalCreatePhone.value
let c = modalCreateJob.value
if ( checkInputs(a, b, c) ) {
console.log('check OK');
// run loader
loaderCreate.style.display = "block"
const urlCreate = 'api/create.php'
dataCreate = {
fio : modalCreateFio.value,
phone : modalCreatePhone.value,
job : modalCreateJob.value
};
sendRequest(urlCreate, dataCreate)
.then((d) => {
if ( d == "Новый контакт создан." ) {
createResult.innerHTML = d
setTimeout(()=> {loaderCreate.style.display = "none"}, 200);
// befor rendering new rows removing old ones
document.querySelectorAll('.row').forEach(e => e.remove());
render();
} else if (d == "Ограничение в количестве контактов.") {
createResult.innerHTML = d
}
console.log(d)
})
.catch((error) => {
console.log(JSON.stringify(error))
});
}
})
// Check inputs function.
function checkInputs(fio, phone, job) {
if ( fio == '' || phone == '' || job == '' ) {
alert("Поле пустое")
return false
}
//+7 980 567-78-87
let pattern = /^[\+]?[0-9]{1}[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{3}[-]?[0-9]{2}[-]?[0-9]{2}$/;
if ( !pattern.test(phone) ) {
alert("Недопустимые символы в поле номера телефона")
return false
}
if ( fio.length > 100 || phone.length > 17 || job.length > 100) {
alert("Количество текстовых символов ограничено 100. Номер телефона ограничен 17.")
return false
}
return true
}
// UPDATE handler
btnEdit.addEventListener("click", function() {
let a = modalEditFio.value
let b = modalEditPhone.value
let c = modalEditJob.value
if ( checkInputs(a, b, c) ) {
const urlEdit = 'api/update.php'
// run loader after click
loaderEdit.style.display = "block"
// data are taken from origin: image attribute "data-..."
// at start images are has rendered by JS with attributes "data-..."
// after click by image dataset copy in inputs of modalEdit
data = {
id:modalEditId,
fio:modalEditFio.value,
phone:modalEditPhone.value,
job:modalEditJob.value
};
sendRequest(urlEdit, data)
.then((d) => {
if ( d == "Контакт был обновлён." ) {
editResult.innerHTML = d
setTimeout(()=> {loaderEdit.style.display = "none"}, 200);
}
document.querySelectorAll('.row').forEach(e => e.remove());
render();
console.log(d)
})
.catch((error) => {
console.log(JSON.stringify(error))
});
}
})
// DELETE handler
// main big button close modal
btnEscape.addEventListener("click", function() {
modalDelete.style.display = "none";
loaderDelete.style.display = "none"
})
// submit deleting
btnDelete.addEventListener("click", function() {
// show loader
loaderDelete.style.display = "block";
const urlDelete = 'api/delete.php'
data = {
id:btnDelete.dataset.delete,
};
sendRequest(urlDelete, data)
.then((d) => {
if ( d == "Контакт был удалён." ) {
deleteResult.innerHTML = d
// if success remove loader
setTimeout(()=> {loaderDelete.style.display = "none"}, 300);
} else {
}
// remove old rows befor inserting
document.querySelectorAll('.row').forEach(e => e.remove());
render();
console.log(d)
})
.catch((error) => {
console.log(JSON.stringify(error))
});
})
// Navigation handler
const nav = document.querySelectorAll('header ul li');
// default settings
task.style.display = "block" // show this TAB at start
solution.style.display = "none"
demo.style.display = "none"
code.style.display = "none"
nav[0].addEventListener("click", function() {
task.style.display = "block"
solution.style.display = "none"
demo.style.display = "none"
code.style.display = "none"
})
nav[1].addEventListener("click", function() {
task.style.display = "none"
solution.style.display = "block"
demo.style.display = "none"
code.style.display = "none"
})
nav[2].addEventListener("click", function() {
task.style.display = "none"
solution.style.display = "none"
demo.style.display = "block"
code.style.display = "none"
})
nav[3].addEventListener("click", function() {
task.style.display = "none"
solution.style.display = "none"
demo.style.display = "none"
code.style.display = "block"
})
// Sending request function
async function sendRequest(urlRead, data) {
// Default options are marked with *
const response = await fetch(urlRead, {
method: 'POST', // .*GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // .*default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *client
body: JSON.stringify(data) // body data type must match "Content-Type" header
});
var json = await response.json()
//console.log('json create: ' + json);
return json
}
// REFRESH DB
refreshDb.addEventListener("click", function() {
data = {}
const urlRefresh = 'api/refresh.php'
sendRequest(urlRefresh, data)
.then((d) => {
if ( d == "База данных была обновлена." ) {
// styling button REFRESH
refreshDb.classList.add("refreshSuccess");
refreshDb.innerHTML = "База данных была обновлена"
} else {
refreshDb.classList.add("refreshError");
refreshDb.innerHTML = "ОШИБКА"
}
// remove old rows befor inserting new
document.querySelectorAll('.row').forEach(e => e.remove());
render();
console.log(d)
setTimeout(()=> {
refreshDb.classList.remove("refreshSuccess");
refreshDb.classList.remove("refreshError");
refreshDb.innerHTML = "Обновить базу данных"
}, 2500);
})
.catch((error) => {
console.log(JSON.stringify(error))
});
})