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

person-icon

Добавить контакт

Цель: «Разработать Телефонный справочник»

Решение

Фронтенд был написан на нативном JavaScript. Для сетевых запросов использовалась технология AJAX, современный и мощный метод fetch(). Создание нового контакта, обновление и удаление происходят без перезагрузки страницы.

Бэкенд написан на РНР, база данных MySQL. Простая валидация входных данных происходит как на клиенте, так и на сервере.

Максимальное количество контактов которое можно записать ограничено до 20.

Данные с контактами обновляются каждые 20 минут. Все изменённые или добавленные записи будут удалены и загрузятся данные по умолчанию.

обновить базу данных

Коды файлов

Файловая структура

file-structure

PHP


// 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);
   }
}
?> 
         

JavaScript


// 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))
   });
})