diff --git a/www/api/v1/.htaccess b/www/api/v1/.htaccess index 6e5e71e..8e4c575 100644 --- a/www/api/v1/.htaccess +++ b/www/api/v1/.htaccess @@ -4,6 +4,8 @@ RewriteEngine On #RewriteRule ^/v1/?([*a-zA-Z0-9_-]+)$ /api/index.php?slug1=$1 [L] RewriteRule ^([*a-zA-Z0-9_-]+)(?:\/|)$ index.php?fi=$1 [L] +RewriteRule ^([*a-zA-Z0-9_-]+)/([*a-zA-Z0-9_-]+)(?:\/|)$ index.php?fi=$1&sc=$2 [L] +RewriteRule ^([*a-zA-Z0-9_-]+)/([*a-zA-Z0-9_-]+)/([*a-zA-Z0-9_-]+)(?:\/|)$ index.php?fi=$1&sc=$2&th=$3 [L] #RewriteRule ^([a-zA-Z0-9_-]+)/$ index.php?slug=$1 [L] #RewriteRule ^project/$ projects/index.php [L] diff --git a/www/api/v1/index.php b/www/api/v1/index.php index 8df1941..096c45e 100644 --- a/www/api/v1/index.php +++ b/www/api/v1/index.php @@ -14,6 +14,19 @@ elseif(isset($data['recipes'])){ } switch ($group){ + case "product": + include 'products.php'; + if(isset($data['sc'])){ $data['q'] = $data['sc']; } + if(isset($data['q'])){ + if(strlen($data['q']) > 2){ + returns( Product::search( $data['q']) ); + } + else { + returnsErr("Please insert another character for the search."); + } + } + break; + case "recipe": include 'recipes.php'; if(!empty($_POST)){ diff --git a/www/api/v1/products.php b/www/api/v1/products.php new file mode 100644 index 0000000..b88e367 --- /dev/null +++ b/www/api/v1/products.php @@ -0,0 +1,154 @@ + $v1){ + if($k1 > 2){ break; } + + $newArr = array(); + $newArr[] = $v1; + foreach ($array as $v2){ + if(!in_array($v2, $newArr)){ + $newArr[] = $v2; + } + } + $arrays[] = $newArr; + + if(count($array) > 2){ + $newArr = array(); + $newArr[] = $v1; + foreach (array_reverse($array) as $v2){ + if(!in_array($v2, $newArr)){ + $newArr[] = $v2; + } + } + $arrays[] = $newArr; + } + } + + return $arrays; +} + + +class Product { + static function addProduct(string $name, string $productGroup){ + + } + + static function search(string $search){ + global $db; + + $results = array(); + $searchWords = explode(' ', $search); + $searchOrg = $search; + $search = str_replace(' ', '%', strtolower($search)); + + $productSql = "SELECT * FROM product WHERE name LIKE '%$search%' ORDER BY IF(name LIKE '$search%', 0, 1)"; + $productSqlRes = $db->query($productSql); + while($row = $productSqlRes->fetch_assoc()){ + $resultArr = array(); + $resultArr['name'] = $row['name']; + $results[] = $resultArr; + if($productSqlRes->num_rows == 1){ + $productId = $row['product_id']; + } + } + + $whereClauseOr = array(); + $whereClauseOr[] = "p.name LIKE '%$search%'"; + foreach (reorderArray($searchWords) as $arr){ + $whereClauseOr[] = "pv.name LIKE '%".implode('%', $arr)."%'"; + } + + $variantSql = " + SELECT + pv.* + FROM + product_variant pv + INNER JOIN product p on pv.product_id = p.product_id + WHERE + ".implode(" OR ", $whereClauseOr)." + ORDER BY + IF(pv.name LIKE '$searchOrg', 0, 1), #exact search result first + IF(pv.name LIKE '$search%', 0, 1) + ;"; + +// echo $variantSql; +// return array(); + + $variantSqlRes = $db->query($variantSql); + while ($row = $variantSqlRes->fetch_assoc()){ + $product = new Product($row['product_id'], $row['name'], $row['variant_id']); + $results[] = $product->toArray(true); + } + + return $results; + } + + private int $product_id; + private int $variant_id; + private string $product_name; + public ?float $price = null; + public ?DateTime $price_updated = null; + public ?int $price_age = null; + + public function __construct(int $product_id, string $product_name, int $variant_id = 0){ + $this->product_id = $product_id; + $this->product_name = $product_name; + $this->variant_id = $variant_id; + } + + public function getPrice(): int|false { + global $db; + if(!empty($this->price)){ return $this->price; } + + // TO DO: Support for store-based-result + $getPriceSql = $db->query("SELECT product_id, date, price, product_variant FROM product_price WHERE product_id = {$this->product_id} AND product_variant = {$this->variant_id} ORDER BY date DESC LIMIT 1"); + + if($getPriceSql->num_rows == 1){ + $row = $getPriceSql->fetch_assoc(); + $this->price = $row['price']; + $this->price_updated = DateTime::createFromFormat('Y-m-d', $row['date']); + $this->price_age = $this->price_updated->diff(new DateTime())->days; + + return $this->price; + } + + return false; + } + + public function toArray($fetchMissingData = false): array { + if($fetchMissingData){ + $this->getPrice(); + } + + $productArray = array(); + $productArray['name'] = $this->product_name; + if($this->price){ + $productArray['price']['price'] = $this->price; + $productArray['price']['updated'] = $this->price_updated->format('Y-m-d'); + $productArray['price']['age'] = $this->price_age; + $productArray['price']['store'] = "store_id"; + } + + return $productArray; + } +} + +class ProductGroup { + private string $name; + +} \ No newline at end of file diff --git a/www/css/index.css b/www/css/index.css index 4a85f77..77f3e90 100644 --- a/www/css/index.css +++ b/www/css/index.css @@ -4,7 +4,7 @@ --wsTop: env(safe-area-inset-top); --wsBottom: constant(safe-area-inset-bottom); --wsBottom: env(safe-area-inset-bottom); - --footerHeight: 51px; + --footerHeight: 72px; /* One line: 51px -> two lines 72px */ } html { @@ -79,6 +79,15 @@ body { height: 38px; } +#stores .card-body { + transition: height 500ms; +} +@media (prefers-reduced-motion) { + #stores .card-body { + transition: none; + } +} + .priceWrapper { float: right; text-align: right; @@ -107,12 +116,36 @@ span.recipeItemAmount::after { } -.itemAmountWrapper { - clear: both; +.list-group-item:focus-within .itemAmountButtons, +.list-group-item:hover .itemAmountButtons +{ + height: 35px; +} +ul.storePlanState .list-group-item:focus-within .itemAmountText, +ul.storePlanState .list-group-item:hover .itemAmountText +{ + height: 0; + overflow: hidden; +} + +.itemAmountButtons { + transition: height 400ms; + overflow: hidden; + height: 0; + width: 100%; +} +@media (prefers-reduced-motion) { + .itemAmountButtons, .itemAmountText { + transition: none; + } } .itemAmountText { font-size: 0.9rem; } +ul.storePlanState .itemAmountText { + height: 35px; + transition: height 400ms; +} .itemAmount.itemAmountBtn { -webkit-appearance: none; -moz-appearance: textfield; @@ -128,6 +161,11 @@ span.recipeItemAmount::after { .addItemForm div.form-control { transition: width 0.5s; } +@media (prefers-reduced-motion) { + .addItemForm div.form-control { + transition: none; + } +} /* .addItemForm div.form-control:not([type="submit"],[type="image"]):focus-within { */ .addItemForm div.form-control:focus-within { width: 40%; @@ -148,6 +186,11 @@ span.recipeItemAmount::after { cursor: pointer; transition: transform 0.5s; } +@media (prefers-reduced-motion) { + .remItem { + transition: none; + } +} .remItem.confirm { transform: rotate(90deg); @@ -176,6 +219,21 @@ span.recipeItemAmount::after { border-radius: 30px; font-weight: bold; } + +.itemSearchDropdown { + width: 100%; + box-shadow: 0 0 2px 2px #2229; + text-align: left; +} +.itemSearchDropdown > li { + border-bottom: 1px solid #5655; + padding: 5pt; + display: flex; + cursor: pointer; +} +.itemSearchDropdown > li:hover { + background: #0004; +} /*}*/ #page-container { /** body ? **/ diff --git a/www/plan/htmlElements.js b/www/plan/htmlElements.js new file mode 100644 index 0000000..03fc6a6 --- /dev/null +++ b/www/plan/htmlElements.js @@ -0,0 +1,59 @@ +class HtmlElements { + + static store = { + editStoreSection: function (name) { + return "