Compare commits

...

8 Commits

Author SHA1 Message Date
17e5f0d5dc Change size column to float type 2022-07-29 12:35:29 +02:00
7742235180 Change price input type 2022-07-29 12:23:30 +02:00
b9bf831001 Fix DDL.sql 2022-07-29 12:15:14 +02:00
b09e8fff28 Remove hard coded credentials 2022-07-29 11:54:09 +02:00
6ce3d64649 Validate forms 2022-07-29 11:33:54 +02:00
16b4d686e3 Make 'Product Add' page 2022-07-28 14:16:55 +02:00
7d2ae0b2d2 Add deleting mechanism 2022-07-28 09:19:11 +02:00
40b7d51b39 Reorganize files 2022-07-28 07:23:22 +02:00
25 changed files with 579 additions and 150 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.phan/

View File

@@ -1,16 +0,0 @@
<html>
<head>
<meta charset="utf-8"/>
<link rel="stylesheet" href="index.css"/>
<title>Product Add</title>
</head>
<body>
<div id="header">
<h1 id="title">Product Add</h1>
<div id="buttons">
<a href="/"><button>Save</button></a>
<a href="/"><button>Cancel</button></a>
</div>
</div>
</body>
</html>

View File

@@ -1,3 +1,5 @@
DROP DATABASE IF EXISTS `products`;
CREATE DATABASE `products`; CREATE DATABASE `products`;
USE `products`; USE `products`;
@@ -9,7 +11,7 @@ CREATE TABLE `product` (
`sku` varchar(100) NOT NULL, `sku` varchar(100) NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `sku` (`sku`) UNIQUE KEY `sku` (`sku`)
) );
CREATE TABLE `book` ( CREATE TABLE `book` (
`id` int unsigned NOT NULL AUTO_INCREMENT, `id` int unsigned NOT NULL AUTO_INCREMENT,
@@ -18,16 +20,16 @@ CREATE TABLE `book` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `book_UN` (`product_id`), UNIQUE KEY `book_UN` (`product_id`),
CONSTRAINT `book_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) CONSTRAINT `book_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`)
) );
CREATE TABLE `dvd` ( CREATE TABLE `dvd` (
`id` int unsigned NOT NULL AUTO_INCREMENT, `id` int unsigned NOT NULL AUTO_INCREMENT,
`product_id` int unsigned NOT NULL, `product_id` int unsigned NOT NULL,
`size` int unsigned NOT NULL, `size` float NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `dvd_UN` (`product_id`), UNIQUE KEY `dvd_UN` (`product_id`),
CONSTRAINT `dvd_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) CONSTRAINT `dvd_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`)
) );
CREATE TABLE `furniture` ( CREATE TABLE `furniture` (
`id` int unsigned NOT NULL AUTO_INCREMENT, `id` int unsigned NOT NULL AUTO_INCREMENT,
@@ -38,5 +40,4 @@ CREATE TABLE `furniture` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `furniture_UN` (`product_id`), UNIQUE KEY `furniture_UN` (`product_id`),
CONSTRAINT `furniture_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) CONSTRAINT `furniture_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`)
) );

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<link rel="stylesheet" href="index.css"/>
<title>Product List</title>
</head>
<body>
<div id="header">
<h1 id="title">Product List</h1>
<div id="buttons">
<a href="add-product"><button>ADD</button></a>
<button id="delete-product-btn">MASS DELETE</button>
</div>
</div>
<div id="products">
</div>
</body>
<script src="index.js"></script>
</html>

View File

@@ -1,45 +0,0 @@
const productsList = document.getElementById('products');
const loadItems = () => {
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
const products = JSON.parse(this.responseText);
const boxes = products.map(product =>
`<div class="product">
<input type="checkbox" class="delete-checkbox" value="${product.id}">
<p>
${product.sku}<br>
${product.name}<br>
${product.price} $<br>
${product.attribute}
</p>
</div>`
)
productsList.innerHTML = boxes.join('\n');
}
xhttp.open('GET', 'products', true);
xhttp.send();
}
loadItems();
const deleteSelected = () => {
const checkboxes = document.querySelectorAll('input[class="delete-checkbox"]:checked');
let values = [];
checkboxes.forEach(checkbox => values.push(checkbox.value));
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
loadItems();
}
xhttp.open('DELETE', 'products', true);
xhttp.send();
}
const deleteButton = document.getElementById('delete-product-btn');
deleteButton.addEventListener('click', deleteSelected);

View File

@@ -1,17 +1,22 @@
<?php <?php
require 'autoload.php'; require 'autoload.php';
ini_set('display_errors', true);
ini_set('error_log', '/tmp/php.log');
use ProductList\Http\Request; use ProductList\Http\Request;
use ProductList\Http\RequestHandler; use ProductList\Http\RequestHandler;
use ProductList\Http\Route; use ProductList\Http\Route;
$request = new Request($_SERVER); $request = new Request($_SERVER, $_GET, $_POST);
$handler = new RequestHandler($request); $handler = new RequestHandler($request);
$handler->registerRoutes([ $handler->registerRoutes([
new Route('GET', 'products', ['ProductList\View\Product', 'listAll']), new Route('GET', 'test', ['ProductList\View\Product', 'test']),
new Route('GET', 'add-product', function() { readfile('add-product.html'); }), new Route('GET', 'product', ['ProductList\View\Product', 'get']),
new Route('GET', '', function() { readfile('index.html'); }), new Route('DELETE', 'product', ['ProductList\View\Product', 'delete']),
new Route('POST', 'product', ['ProductList\View\Product', 'post']),
new Route('GET', 'add-product', function() { readfile('static/add-product.html'); }),
new Route('GET', '', function() { readfile('static/index.html'); }),
]); ]);
$handler->handle(); $handler->handle();

View File

@@ -0,0 +1,4 @@
<?php
namespace ProductList\Exception;
class DuplicateException extends \Exception {}

View File

@@ -0,0 +1,11 @@
<?php
namespace ProductList\Exception;
class InvalidFieldException extends \Exception
{
public function __construct($field, $code = 0, Throwable $previous = null)
{
http_response_code(400);
parent::__construct("The field '$field' is invalid.", $code, $previous);
}
}

View File

@@ -0,0 +1,4 @@
<?php
namespace ProductList\Exception;
class NotFoundException extends \Exception {}

View File

@@ -5,6 +5,8 @@ class Request
{ {
private $method; private $method;
private $uri; private $uri;
private $queryParams;
private $formParams;
public function getMethod() public function getMethod()
{ {
@@ -16,9 +18,25 @@ class Request
return $this->uri; return $this->uri;
} }
public function __construct(array $params) public function getQueryParams()
{ {
$this->uri = basename($params['REQUEST_URI']); return $this->queryParams;
}
public function getFormParams()
{
return $this->formParams;
}
public function __construct(array $params, array $queryParams, array $formParams)
{
$uri_base = strtok($params['REQUEST_URI'], '?');
$uri_base = trim(urldecode($uri_base), '/');
$this->uri = explode('/', $uri_base);
$this->method = $params['REQUEST_METHOD']; $this->method = $params['REQUEST_METHOD'];
$this->queryParams = $queryParams;
$this->formParams = $formParams;
} }
} }

View File

@@ -20,7 +20,12 @@ class RequestHandler
{ {
foreach ($this->routes as $route) { foreach ($this->routes as $route) {
if ($route->matches($this->request)) { if ($route->matches($this->request)) {
try {
$route->execute($this->request); $route->execute($this->request);
} catch (\Exception $e) {
http_response_code(500);
echo $e->getMessage();
}
return; return;
} }
} }

View File

@@ -10,7 +10,7 @@ class Route
public function __construct(string $method, string $uri, array|\Closure $view) public function __construct(string $method, string $uri, array|\Closure $view)
{ {
$this->method = $method; $this->method = $method;
$this->uri = $uri; $this->uri = explode('/', $uri);
$this->view = $view; $this->view = $view;
} }

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace ProductList\Model; namespace ProductList\Model;
use ProductList\Exception\InvalidFieldException;
class Book extends Product class Book extends Product
{ {
private $weight; private $weight;
@@ -14,6 +16,11 @@ class Book extends Product
$variationId = null $variationId = null
) { ) {
parent::__construct($sku, $name, $price, $productId, $variationId); parent::__construct($sku, $name, $price, $productId, $variationId);
if (empty($weight) || !is_numeric($weight)) {
throw new InvalidFieldException('Weight');
}
$this->weight = $weight; $this->weight = $weight;
} }
@@ -24,8 +31,8 @@ class Book extends Product
$row['name'], $row['name'],
$row['price'], $row['price'],
$row['weight'], $row['weight'],
$row['product_id'], $row['productId'],
$row['variation_id'] $row['variationId']
); );
} }
@@ -42,8 +49,15 @@ class Book extends Product
private static function getSelectAllQuery() : string private static function getSelectAllQuery() : string
{ {
return 'SELECT '.PRODUCT.'.*, '.BOOK.'.id as variation_id, size return '
FROM '.PRODUCT.' LEFT JOIN '.BOOK.' ON '.PRODUCT.'.id = '.BOOK.'.product_id'; SELECT
'.PRODUCT.'.*,
'.BOOK.'.id as variationId,
weight
FROM
'.PRODUCT.'
LEFT JOIN
'.BOOK.' ON '.PRODUCT.'.id = '.BOOK.'.product_id';
} }
public function insert($conn = null) : int public function insert($conn = null) : int
@@ -66,7 +80,24 @@ class Book extends Product
$this->setVariationId($conn->insert_id); $this->setVariationId($conn->insert_id);
return $conn->insert_id; return $conn->insert_id;
} else { } else {
throw new Exception("Unable to insert object"); throw new \Exception("Unable to insert object");
} }
} }
public function delete($conn = null)
{
if ($conn === null) {
$conn = Database::connect();
}
$variationId = $this->getVariationId();
$stmt = $conn->prepare('DELETE FROM '.BOOK.' WHERE id = ?');
$stmt->bind_param('i', $variationId);
if ($stmt->execute() === false) {
throw new \Exception("Unable to delete product with id '$variationId'");
}
parent::delete($conn);
}
} }

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace ProductList\Model; namespace ProductList\Model;
use ProductList\Exception\InvalidFieldException;
class DVD extends Product class DVD extends Product
{ {
private $size; private $size;
@@ -14,6 +16,11 @@ class DVD extends Product
$variationId = null $variationId = null
) { ) {
parent::__construct($sku, $name, $price, $productId, $variationId); parent::__construct($sku, $name, $price, $productId, $variationId);
if (empty($size) || !is_numeric($size)) {
throw new InvalidFieldException('Size');
}
$this->size = $size; $this->size = $size;
} }
@@ -24,8 +31,8 @@ class DVD extends Product
$row['name'], $row['name'],
$row['price'], $row['price'],
$row['size'], $row['size'],
$row['product_id'], $row['productId'],
$row['variation_id'] $row['variationId']
); );
} }
@@ -42,8 +49,15 @@ class DVD extends Product
private static function getSelectAllQuery() : string private static function getSelectAllQuery() : string
{ {
return 'SELECT '.PRODUCT.'.*, '.DVD.'.id as variation_id, size return '
FROM '.PRODUCT.' LEFT JOIN '.DVD.' ON '.PRODUCT.'.id = '.DVD.'.product_id'; SELECT
'.PRODUCT.'.*,
'.DVD.'.id as variationId,
size
FROM
'.PRODUCT.'
LEFT JOIN
'.DVD.' ON '.PRODUCT.'.id = '.DVD.'.product_id';
} }
public function insert($conn = null) : int public function insert($conn = null) : int
@@ -60,13 +74,30 @@ class DVD extends Product
$size = $this->getSize(); $size = $this->getSize();
$stmt = $conn->prepare("INSERT INTO ".DVD." (product_id, size) VALUES (?, ?);"); $stmt = $conn->prepare("INSERT INTO ".DVD." (product_id, size) VALUES (?, ?);");
$stmt->bind_param('ii', $productId, $size); $stmt->bind_param('id', $productId, $size);
if ($stmt->execute() === true) { if ($stmt->execute() === true) {
$this->setVariationId($conn->insert_id); $this->setVariationId($conn->insert_id);
return $conn->insert_id; return $conn->insert_id;
} else { } else {
throw new Exception("Unable to insert object"); throw new \Exception("Unable to insert object");
} }
} }
public function delete($conn = null)
{
if ($conn === null) {
$conn = Database::connect();
}
$variationId = $this->getVariationId();
$stmt = $conn->prepare('DELETE FROM '.DVD.' WHERE id = ?');
$stmt->bind_param('i', $variationId);
if ($stmt->execute() === false) {
throw new \Exception("Unable to delete product with id '$variationId'");
}
parent::delete($conn);
}
} }

View File

@@ -8,15 +8,10 @@ define("FURNITURE", "furniture");
class Database class Database
{ {
const SERVERNAME = "127.0.0.1";
const DATABASE = "scandiweb";
const USERNAME = "root";
const PASSWORD = "root";
public static function connect() public static function connect()
{ {
$conn = new \mysqli(self::SERVERNAME, self::USERNAME, self::PASSWORD); $conn = new \mysqli(getenv('SERVERNAME'), getenv('USERNAME'), getenv('PASSWORD'));
$conn->select_db(self::DATABASE); $conn->select_db(getenv('DATABASE'));
return $conn; return $conn;
} }
} }

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace ProductList\Model; namespace ProductList\Model;
use ProductList\Exception\InvalidFieldException;
class Furniture extends Product class Furniture extends Product
{ {
private $height; private $height;
@@ -8,7 +10,7 @@ class Furniture extends Product
private $length; private $length;
public function __construct( public function __construct(
$SKU, $sku,
$name, $name,
$price, $price,
$height, $height,
@@ -17,7 +19,20 @@ class Furniture extends Product
$productId = null, $productId = null,
$variationId = null $variationId = null
) { ) {
parent::__construct($SKU, $name, $price, $productId, $variationId); parent::__construct($sku, $name, $price, $productId, $variationId);
if (empty($height) || !is_numeric($height)) {
throw new InvalidFieldException('Height');
}
if (empty($width) || !is_numeric($width)) {
throw new InvalidFieldException('Width');
}
if (empty($length) || !is_numeric($length)) {
throw new InvalidFieldException('Length');
}
$this->height = $height; $this->height = $height;
$this->width = $width; $this->width = $width;
$this->length = $length; $this->length = $length;
@@ -32,8 +47,8 @@ class Furniture extends Product
$row['height'], $row['height'],
$row['width'], $row['width'],
$row['length'], $row['length'],
$row['product_id'], $row['productId'],
$row['variation_id'] $row['variationId']
); );
} }
@@ -62,9 +77,17 @@ class Furniture extends Product
private static function getSelectAllQuery() : string private static function getSelectAllQuery() : string
{ {
return 'SELECT '.PRODUCT.'.*, '.FURNITURE.'.id as variation_id, size return '
FROM '.PRODUCT.' SELECT
LEFT JOIN '.FURNITURE.' ON '.PRODUCT.'.id = '.FURNITURE.'.product_id'; '.PRODUCT.'.*,
'.FURNITURE.'.id as variationId,
height,
width,
length
FROM
'.PRODUCT.'
LEFT JOIN '.FURNITURE.' ON
'.PRODUCT.'.id = '.FURNITURE.'.product_id';
} }
public function insert($conn = null) : int public function insert($conn = null) : int
@@ -92,7 +115,24 @@ class Furniture extends Product
$this->setVariationId($conn->insert_id); $this->setVariationId($conn->insert_id);
return $conn->insert_id; return $conn->insert_id;
} else { } else {
throw new Exception("Unable to insert object"); throw new \Exception("Unable to insert object");
} }
} }
public function delete($conn = null)
{
if ($conn === null) {
$conn = Database::connect();
}
$variationId = $this->getVariationId();
$stmt = $conn->prepare('DELETE FROM '.FURNITURE.' WHERE id = ?');
$stmt->bind_param('i', $variationId);
if ($stmt->execute() === false) {
throw new \Exception("Unable to delete product with id '$variationId'");
}
parent::delete($conn);
}
} }

View File

@@ -5,6 +5,7 @@ trait Model
{ {
abstract public static function fromRow($row) : self; abstract public static function fromRow($row) : self;
abstract public function insert($conn = null) : int; // should return id abstract public function insert($conn = null) : int; // should return id
abstract public function delete($conn = null);
abstract private static function getSelectAllQuery() : string; abstract private static function getSelectAllQuery() : string;
public static function selectAll($conn = null) : array public static function selectAll($conn = null) : array

View File

@@ -1,32 +1,50 @@
<?php <?php
namespace ProductList\Model; namespace ProductList\Model;
use ProductList\Exception\NotFoundException;
use ProductList\Exception\InvalidFieldException;
use ProductList\Exception\DuplicateException;
abstract class Product implements \JsonSerializable abstract class Product implements \JsonSerializable
{ {
use Model; use Model;
private $variationId; private $variationId;
private $SKU; private $sku;
private $name; private $name;
private $price; private $price;
private $productId; private $productId;
public function __construct( public function __construct(
$SKU, $sku,
$name, $name,
$price, $price,
$productId = null, $productId = null,
$variationId = null $variationId = null
) { ) {
if (empty($sku)) {
throw new InvalidFieldException('SKU');
}
if (empty($name)) {
throw new InvalidFieldException('Name');
}
if (empty($price) || !is_numeric($price)) {
throw new InvalidFieldException('Price');
}
$this->productId = $productId; $this->productId = $productId;
$this->variationId = $variationId; $this->variationId = $variationId;
$this->SKU = $SKU;
$this->sku = $sku;
$this->name = $name; $this->name = $name;
$this->price = $price; $this->price = $price;
} }
public function getSKU() public function getSKU()
{ {
return $this->SKU; return $this->sku;
} }
public function getName() public function getName()
@@ -44,6 +62,11 @@ abstract class Product implements \JsonSerializable
return $this->productId; return $this->productId;
} }
public function getVariationId()
{
return $this->variationId;
}
public function setVariationId($id) public function setVariationId($id)
{ {
$this->variationId = $id; $this->variationId = $id;
@@ -56,28 +79,84 @@ abstract class Product implements \JsonSerializable
abstract public function getFormatedAttr(); abstract public function getFormatedAttr();
public function delete($conn = null)
{
if ($conn === null) {
$conn = Database::connect();
}
$productId = $this->getProductId();
$stmt = $conn->prepare('DELETE FROM '.PRODUCT.' WHERE id = ?');
$stmt->bind_param('i', $productId);
if ($stmt->execute() === false) {
throw new \Exception("Unable to delete product with id '$productId'");
}
}
public static function fromId($id, $conn = null) : self
{
if ($conn === null) {
$conn = Database::connect();
}
$stmt = $conn->prepare(self::getSelectAllQuery().' WHERE '.PRODUCT.'.id = ?');
$stmt->bind_param('i', $id);
if ($stmt->execute() === true) {
$row = $stmt->get_result()->fetch_assoc();
if($row === null) {
throw new NotFoundException("No product with id '$id'");
}
return self::fromRow($row);
} else {
throw new \Exception("Unable to select object");
}
}
public static function fromRow($row) : self public static function fromRow($row) : self
{ {
if ($row['size'] !== null) { switch ($row['productType']) {
case 'dvd':
return DVD::fromRow($row); return DVD::fromRow($row);
} elseif ($row['weight'] !== null) { case 'book':
return Book::fromRow($row); return Book::fromRow($row);
} elseif ($row['height'] !== null) { case 'furniture':
return Furniture::fromRow($row); return Furniture::fromRow($row);
} else { default:
throw new Exception("Product without a type"); throw new \Exception("Product without a type");
} }
} }
private static function getSelectAllQuery() : string private static function getSelectAllQuery() : string
{ {
return 'SELECT '.PRODUCT.'.id as product_id, return '
COALESCE('.DVD.'.id, '.BOOK.'.id, '.FURNITURE.'.id) as variation_id, SELECT
name, sku, price, size, weight, width, height, length '.PRODUCT.'.id as productId,
FROM '.PRODUCT.' name,
LEFT JOIN '.DVD.' ON '.PRODUCT.'.id = '.DVD.'.product_id sku,
LEFT JOIN '.BOOK.' ON '.PRODUCT.'.id = '.BOOK.'.product_id price,
LEFT JOIN '.FURNITURE.' ON '.PRODUCT.'.id = '.FURNITURE.'.product_id;'; size,
weight,
width,
height,
length,
COALESCE('.DVD.'.id, '.BOOK.'.id, '.FURNITURE.'.id) as variationId,
CASE
WHEN '.DVD.'.id IS NOT NULL THEN "dvd"
WHEN '.BOOK.'.id IS NOT NULL THEN "book"
WHEN '.FURNITURE.'.id IS NOT NULL THEN "furniture"
END as productType
FROM
'.PRODUCT.'
LEFT JOIN '.DVD.' ON
'.PRODUCT.'.id = '.DVD.'.product_id
LEFT JOIN '.BOOK.' ON
'.PRODUCT.'.id = '.BOOK.'.product_id
LEFT JOIN '.FURNITURE.' ON
'.PRODUCT.'.id = '.FURNITURE.'.product_id';
} }
public function insert($conn = null) : int public function insert($conn = null) : int
@@ -96,11 +175,19 @@ abstract class Product implements \JsonSerializable
); );
$stmt->bind_param('ssd', $SKU, $name, $price); $stmt->bind_param('ssd', $SKU, $name, $price);
try {
if ($stmt->execute() === true) { if ($stmt->execute() === true) {
$this->setProductId($conn->insert_id); $this->setProductId($conn->insert_id);
return $conn->insert_id; return $conn->insert_id;
} else { } else {
throw new Exception("Unable to insert object"); throw new \Exception("Unable to insert object");
}
} catch (\mysqli_sql_exception $e) {
if ($e->getCode() === 1062) {
throw new DuplicateException('The provided SKU is already registered.');
} else {
throw $e;
}
} }
} }

View File

@@ -3,11 +3,69 @@ namespace ProductList\View;
use ProductList\Http\Request; use ProductList\Http\Request;
use ProductList\Model\Product as ProductModel; use ProductList\Model\Product as ProductModel;
use ProductList\Exception\NotFoundException;
class Product class Product extends View
{ {
public static function listAll(Request $request) public static function get(Request $request)
{ {
header('Content-Type: application/json');
echo json_encode(ProductModel::selectAll()); echo json_encode(ProductModel::selectAll());
} }
public static function delete(Request $request)
{
$queryParams = $request->getQueryParams();
if (array_key_exists('id', $queryParams)) {
$ids = explode(',', $queryParams['id']);
$ids = array_map('intval', $ids);
foreach($ids as $id) {
try {
$product = ProductModel::fromId($id);
try {
$product->delete();
} catch (\Exception $e) {
http_response_code(500);
echo $e->getMessage();
return;
}
} catch (NotFoundException $e) {
http_response_code(404);
echo "The selected(s) object(s) is(are) not available anymore.";
return;
}
}
} else {
http_response_code(400);
echo 'Missing parameter "id".';
}
}
public static function post(Request $request)
{
$params= $request->getFormParams();
$expected = [
'sku',
'name',
'price',
'productType',
'weight',
'size',
'height',
'width',
'length'
];
if (self::expectArgs($expected, $params)) {
$params['productId'] = null;
$params['variationId'] = null;
$product = ProductModel::fromRow($params);
$product->insert();
}
}
} }

22
src/View/View.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
namespace ProductList\View;
abstract class View
{
protected static function expectArgs(array $args, array $provided) {
$keys = array_keys($provided);
$missing = array_diff($args, $keys);
if (count($missing) > 0) {
http_response_code(400);
$missing = join("', '", $missing);
echo "Missing parameters '$missing'";
return false;
}
return true;
}
}

83
static/add-product.html Normal file
View File

@@ -0,0 +1,83 @@
<html>
<head>
<meta charset="utf-8"/>
<link rel="stylesheet" href="static/stylesheet.css"/>
<title>Product Add</title>
</head>
<body>
<div id="header">
<h1 id="title">Product Add</h1>
<div id="buttons">
<div class="header-button"><button type="submit" form="product_form" id="save">Save</button></div>
<div class="header-button"><a href="/"><button>Cancel</button></a></div>
</div>
</div>
<div id="main">
<form id="product_form">
<div id="basic-info" class="form-section">
<table>
<tr>
<td><label for="sku">SKU</label></td>
<td><input type="text" id="sku" name="sku" required></td>
</tr>
<tr>
<td><label for="name">Name</label></td>
<td><input type="text" id="name" name="name" required></td>
</tr>
<tr>
<td><label for="price">Price ($)</label></td>
<td><input type="text" id="price" name="price" required></td>
</tr>
</table>
</div>
<div id="type-switcher" class="form-section">
<label for="productType">Type Switcher</label>
<select name="productType" id="productType">
<option value="dvd">DVD</option>
<option value="furniture">Furniture</option>
<option value="book">Book</option>
</select>
</div>
<div id="attribute" class="form-section">
<div id="dvd" class="hidden">
<table>
<tr>
<td><label for="size">Size (MB)</label></td>
<td><input type="text" id="size" name="size"></td>
</tr>
</table>
Please, provide size
</div>
<div id="furniture" class="hidden">
<table>
<tr>
<td><label for="height">Height (CM)</label></td>
<td><input type="text" id="height" name="height"></td>
</tr>
<tr>
<td><label for="width">Width (CM)</label></td>
<td><input type="text" id="width" name="width"></td>
</tr>
<tr>
<td><label for="length">Length (CM)</label></td>
<td><input type="text" id="length" name="length"></td>
</tr>
</table>
Please, provide dimensions
</div>
<div id="book" class="hidden">
<table>
<tr>
<td><label for="weight">Weight (KG)</label></td>
<td><input type="text" id="weight" name="weight"></td>
</tr>
</table>
Please, provide weight
</div>
</div>
</form>
</div>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="static/add-product.js"></script>
</html>

29
static/add-product.js Normal file
View File

@@ -0,0 +1,29 @@
let currentTypeForm = document.getElementById($('#productType').val());
currentTypeForm.querySelectorAll('input').forEach(input => input.required = true);
currentTypeForm.classList.remove('hidden');
$('#productType').on('change', e => {
const newTypeForm = document.getElementById(e.target.value);
currentTypeForm.classList.add('hidden');
newTypeForm.classList.remove('hidden');
currentTypeForm.querySelectorAll('input').forEach(input => input.required = false);
newTypeForm.querySelectorAll('input').forEach(input => input.required = true);
currentTypeForm = newTypeForm;
});
$('#product_form').on('submit', e=> {
e.preventDefault();
$.ajax(
'product',
{
method: 'POST',
data: $('#product_form').serializeArray(),
success: _ => window.location.href = '/',
error: jqXHR => alert(jqXHR.responseText),
}
);
});

21
static/index.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<link rel="stylesheet" href="static/stylesheet.css"/>
<title>Product List</title>
</head>
<body>
<div id="header">
<h1 id="title">Product List</h1>
<div id="buttons">
<div class="header-button"><a href="add-product"><button>ADD</button></a></div>
<div class="header-button"><button id="delete-product-btn">MASS DELETE</button></div>
</div>
</div>
<div id="products">
</div>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="static/index.js"></script>
</html>

44
static/index.js Normal file
View File

@@ -0,0 +1,44 @@
const loadItems = () => {
$.ajax(
'product',
{
success: data => {
const boxes = data.map(product =>
`<div class="product">
<input type="checkbox" class="delete-checkbox" value="${product.id}">
<p>
${product.sku}<br>
${product.name}<br>
${product.price} $<br>
${product.attribute}
</p>
</div>`
);
$('#products').html(boxes.join('\n'));
},
error: jqXHR => alert(jqXHR.responseText),
}
)
}
loadItems();
$('#delete-product-btn').on('click', () => {
let values = [];
const checkboxes = document.querySelectorAll('input[class="delete-checkbox"]:checked');
checkboxes.forEach(checkbox => values.push(checkbox.value));
if(values.length) {
$.ajax(
`product?id=${values.join(',')}`,
{
method: 'DELETE',
success: loadItems,
error: jqXHR => alert(jqXHR.responseText),
}
)
} else {
alert('Please select a product.');
}
});

View File

@@ -14,16 +14,15 @@ body {
margin: 0 1em; margin: 0 1em;
} }
#buttons a { .header-button {
align-self: center; align-self: center;
margin: 0 2em;
} }
#buttons button { #buttons button {
font-family: cursive; font-family: cursive;
padding: .5em 1em; padding: .5em 1em;
font-size: 1rem; font-size: 1rem;
margin: 0 2em;
align-self: center;
background-color: white; background-color: white;
border: 2px solid black; border: 2px solid black;
box-shadow: 2px 2px black; box-shadow: 2px 2px black;
@@ -56,3 +55,23 @@ body {
position: absolute; position: absolute;
top: 0px; top: 0px;
} }
.hidden {
display: none;
}
#main {
margin: 2em;
}
.form-section {
margin: 2em;
}
td:nth-child(2) {
padding-left: 2em;
}
#productType {
margin-left: 2em;
}