Compare commits
8 Commits
922b29519a
...
master
Author | SHA1 | Date | |
---|---|---|---|
17e5f0d5dc | |||
7742235180 | |||
b9bf831001 | |||
b09e8fff28 | |||
6ce3d64649 | |||
16b4d686e3 | |||
7d2ae0b2d2 | |||
40b7d51b39 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.phan/
|
@@ -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>
|
|
@@ -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`)
|
||||||
)
|
);
|
||||||
|
|
20
index.html
20
index.html
@@ -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>
|
|
45
index.js
45
index.js
@@ -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);
|
|
13
index.php
13
index.php
@@ -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();
|
||||||
|
4
src/Exception/DuplicateException.php
Normal file
4
src/Exception/DuplicateException.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
namespace ProductList\Exception;
|
||||||
|
|
||||||
|
class DuplicateException extends \Exception {}
|
11
src/Exception/InvalidFieldException.php
Normal file
11
src/Exception/InvalidFieldException.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
4
src/Exception/NotFoundException.php
Normal file
4
src/Exception/NotFoundException.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
namespace ProductList\Exception;
|
||||||
|
|
||||||
|
class NotFoundException extends \Exception {}
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
22
src/View/View.php
Normal 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
83
static/add-product.html
Normal 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
29
static/add-product.js
Normal 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
21
static/index.html
Normal 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
44
static/index.js
Normal 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.');
|
||||||
|
}
|
||||||
|
});
|
@@ -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;
|
||||||
|
}
|
Reference in New Issue
Block a user