UI-fixes and product-search on item-add. (Products can't be added yet, though)
+ Now reduces motion if that's what the user wants. + The change-amount elements are hidden unless you want to edit them.master
parent
02bd4d4131
commit
7d57b449b4
|
@ -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]
|
||||
|
|
|
@ -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)){
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
|
||||
function numberLowest( ...$nums ){
|
||||
$lowest = null;
|
||||
|
||||
foreach ($nums as $num){
|
||||
if($num < $lowest || $lowest == null){
|
||||
$lowest = $num;
|
||||
}
|
||||
}
|
||||
|
||||
return $lowest;
|
||||
}
|
||||
|
||||
function reorderArray($array): array {
|
||||
$arrays = array();
|
||||
|
||||
foreach ($array as $k1 => $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;
|
||||
|
||||
}
|
|
@ -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 ? **/
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
class HtmlElements {
|
||||
|
||||
static store = {
|
||||
editStoreSection: function (name) {
|
||||
return "<div class=\"editStoreSection\" style=\"display: none; padding: 10px; overflow: hidden;\">\n" +
|
||||
" <form class='editStoreForm'>\n" +
|
||||
" <div class=\"row g-3 mb-3 align-items-center\">\n" +
|
||||
" <div class=\"col-auto\">\n" +
|
||||
" <label for=\"editStoreName\" class=\"col-form-label\">Store name:</label>\n" +
|
||||
" </div>\n" +
|
||||
" <div class=\"col-auto\">\n" +
|
||||
" <input type=\"text\" id=\"editStoreName\" class=\"form-control\" value='"+name+"'>\n" +
|
||||
" </div>\n" +
|
||||
" </div>\n" +
|
||||
" <div class=\"row g-3 mb-3 align-items-center\">\n" +
|
||||
" <div class=\"col-auto\">\n" +
|
||||
" <label for=\"editStoreChain\" class=\"col-form-label\">Chain:</label>\n" +
|
||||
" </div>\n" +
|
||||
" <div class=\"col-auto\">\n" +
|
||||
" <select id=\"editStoreChain\" class=\"form-select\">\n" +
|
||||
" <option>--Select a chain--</option>\n" +
|
||||
" <option>Rema 1000</option>\n" +
|
||||
" <option>Coop Obs</option>\n" +
|
||||
" <option>Coop Extra</option>\n" +
|
||||
" </select>\n" +
|
||||
" </div>\n" +
|
||||
" </div>\n" +
|
||||
" <button type='submit' class='btn btn-primary'>Save</button>\n" +
|
||||
" </form>\n" +
|
||||
" <hr>\n" +
|
||||
"</div>";
|
||||
},
|
||||
editStoreSectionEvents: function(){
|
||||
},
|
||||
|
||||
items: {
|
||||
|
||||
},
|
||||
|
||||
addItemSection:
|
||||
"<span class='addItemFormWrapper'>\n" +
|
||||
" <hr>\n" +
|
||||
" <form action='#!' class='form-row input-group input-group-sm addItemForm'>\n" +
|
||||
" <div class='form-control form-floating'>\n" +
|
||||
" <input type='text' id='newItemName0' class='form-control newItemName' placeholder='New Item Name' aria-label='New Item Name' autocomplete='off' autocapitalize='on'>\n" +
|
||||
" <label for='newItemName0'>New Item Name</label>\n" +
|
||||
" </div>\n" +
|
||||
" <div class='form-control form-floating'>\n" +
|
||||
" <input type='number' id='newItemPrice0' class='form-control newItemPrice' value='0' min='0' step='.01' aria-label='Price'>\n" +
|
||||
" <label for='newItemPrice0'>Price</label>\n" +
|
||||
" </div>\n" +
|
||||
" <div class='input-group-append'>\n" +
|
||||
" <input type='image' class='form-control addItem' src='../icon/plus.svg' alt='+'>\n" +
|
||||
" </div>\n" +
|
||||
// " <div class='itemSearchDropdown' style='display: none;'><ul></ul></div>\n" +
|
||||
" </form>\n" +
|
||||
"</span>"
|
||||
}
|
||||
}
|
|
@ -33,8 +33,10 @@
|
|||
|
||||
</div>
|
||||
|
||||
<script src='htmlElements.js'></script>
|
||||
<script src='plan.js'></script>
|
||||
<script src='recipe.js'></script>
|
||||
<script src='product.js'></script>
|
||||
<script src='draggingClass.js'></script>
|
||||
</div>
|
||||
<?php include $rPath.'webdata/footer.html'; ?>
|
||||
|
|
100
www/plan/plan.js
100
www/plan/plan.js
|
@ -1,5 +1,7 @@
|
|||
/*jshint sub:true, esversion: 6, -W083 */
|
||||
|
||||
// Default timer for jquery slide, except set to 0 if the user prefers reduced motion.
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const slideTimer = prefersReducedMotion?0:400;
|
||||
class Store {
|
||||
constructor(title, storeID, state) {
|
||||
this.itemsObj = {};
|
||||
|
@ -30,24 +32,7 @@ class Store {
|
|||
html += " <li class='list-group-item draggable' data-itemid='0' data-storeid='"+this.storeID+"' style='height: 10px; width: 100%;'></li>";
|
||||
html += " </ul>";
|
||||
|
||||
html += " <span class='addItemFormWrapper'>";
|
||||
html += " <hr>";
|
||||
html += " <form action='#!' class='form-row input-group input-group-sm addItemForm'>";
|
||||
// html += " <input type='text' class='form-control newItemName' placeholder='New Item Name' data-toggle='tooltip' title='New Item Name' aria-label='New Item Name'>";
|
||||
// html += " <input type='number' class='form-control newItemPrice' value='0' min='0' step='.01' data-toggle='tooltip' title='Price' aria-label='Price'>";
|
||||
html += " <div class='form-control form-floating'>";
|
||||
html += " <input type='text' id='newItemName0' class='form-control newItemName' placeholder='New Item Name' aria-label='New Item Name' autocomplete='off' autocapitalize='on'>";
|
||||
html += " <label for='newItemName0'>New Item Name</label>";
|
||||
html += " </div>";
|
||||
html += " <div class='form-control form-floating'>";
|
||||
html += " <input type='number' id='newItemPrice0' class='form-control newItemPrice' value='0' min='0' step='.01' aria-label='Price'>";
|
||||
html += " <label for='newItemPrice0'>Price</label>";
|
||||
html += " </div>";
|
||||
html += " <div class='input-group-append'>";
|
||||
html += " <input type='image' class='form-control addItem' src='../icon/plus.svg' alt='+'>";
|
||||
html += " </div>";
|
||||
html += " </form>";
|
||||
html += " </span>";
|
||||
html += HtmlElements.store.addItemSection;
|
||||
|
||||
html += " </div>";
|
||||
html += " <div class='card-footer subtotal'>Subtotal: <span class='priceWrapper price'>0.00</span></div>";
|
||||
|
@ -55,6 +40,10 @@ class Store {
|
|||
|
||||
this.selector = $(html).appendTo("#stores");
|
||||
|
||||
HtmlElements.store.editStoreSectionEvents();
|
||||
|
||||
this.selector.find(".newItemName").on('keyup', productSearchEvent);
|
||||
|
||||
this.selector.find(".addItemForm").on('submit', ev => {
|
||||
ev.preventDefault();
|
||||
|
||||
|
@ -73,19 +62,22 @@ class Store {
|
|||
|
||||
if($(ev.currentTarget).hasClass('planningState')){
|
||||
this.setState('planning');
|
||||
this.selector.find(".newItemName").first().focus();
|
||||
// this.selector.find(".newItemName").first().focus();
|
||||
this.selector.find(".card-body").focus();
|
||||
}
|
||||
else if($(ev.currentTarget).hasClass('shoppingState')){
|
||||
this.setState('shopping');
|
||||
this.selector.find(".checkItems input").first().focus();
|
||||
// this.selector.find(".checkItems input").first().focus();
|
||||
this.selector.find(".card-body").focus();
|
||||
}
|
||||
else if($(ev.currentTarget).hasClass('closedState')){
|
||||
this.setState('closed');
|
||||
this.selector.find(".removeStore").first().focus();
|
||||
this.selector.find(".card-body").focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.selector.find('.editStoreName').one('click', ev => { this.editNameFn(ev); });
|
||||
// this.selector.find('.editStoreName').on('click', ev => { this.toggleEditStore(); });
|
||||
|
||||
this.selector.find('.removeStore').on('click', ev => {
|
||||
if(confirm("Are you sure you want to remove this store?")){ this.removeStore(); }
|
||||
|
@ -113,22 +105,24 @@ class Store {
|
|||
}
|
||||
|
||||
setState(state, animTime){
|
||||
animTime = animTime || 200;
|
||||
animTime = prefersReducedMotion?0:(animTime || 200);
|
||||
let prevState = this.state;
|
||||
|
||||
if(state === "planning"){
|
||||
this.state = "planning";
|
||||
this.selector.find('ul').addClass('storePlanState');
|
||||
this.selector.find('li:not(.checkedItem) .itemAmountButtons').slideDown(animTime);
|
||||
this.selector.find('li:not(.checkedItem) .itemAmountText').slideUp(animTime);
|
||||
this.selector.find('.checkedItem .itemAmountText:not(.oneItem)').slideDown(animTime);
|
||||
// this.selector.find('li:not(.checkedItem) .itemAmountText').slideUp(animTime);
|
||||
// this.selector.find('.checkedItem .itemAmountText:not(.oneItem)').slideDown(animTime);
|
||||
|
||||
this.selector.find('.addItemFormWrapper').slideDown(animTime);
|
||||
this.selector.find('.remItem').show();
|
||||
this.draggingClass.unpause();
|
||||
}
|
||||
if(state !== "planning"){
|
||||
this.selector.find('ul').removeClass('storePlanState');
|
||||
this.selector.find('.itemAmountButtons').slideUp(animTime);
|
||||
this.selector.find('.itemAmountText:not(.oneItem)').slideDown(animTime);
|
||||
// this.selector.find('.itemAmountText:not(.oneItem)').slideDown(animTime);
|
||||
this.selector.find('.addItemFormWrapper').slideUp(animTime);
|
||||
this.selector.find('.remItem').hide();
|
||||
this.draggingClass.pause();
|
||||
|
@ -147,7 +141,7 @@ class Store {
|
|||
|
||||
}
|
||||
if(state !== "closed"){
|
||||
|
||||
this.selector.find('.archiveSection').slideUp(animTime);
|
||||
}
|
||||
|
||||
if(prevState !== state){
|
||||
|
@ -234,31 +228,37 @@ class Store {
|
|||
price = Number(price);
|
||||
this.itemsObj[itemID] = { text: text, price: price, itemID: itemID, amount: amount, checked: checked };
|
||||
|
||||
let html = "\n";
|
||||
html += "<li class='list-group-item draggable"+(checked?' checkedItem':'')+"' id='item_"+itemID+"' data-itemid='"+itemID+"' style='height: 100%; min-width: 200px;'>"; // draggable='true'
|
||||
// html += " <span style='float: left; margin-right: 0px; width: 32px; margin-left: -22px;'><svg xmlns='http://www.w3.org/2000/svg' fill='gray' class='bi bi-grip-vertical' viewBox='0 0 16 16' height='32' width='32'> <path d='M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z'></path></svg></span>";
|
||||
html += " <span style='float: left; display: none; margin-right: 5px;' class='checkItems'><input type='checkbox'"+(checked?" checked":"")+"></span>";
|
||||
html += " <span style='float: left;'>"+text+"</span>";
|
||||
let html =
|
||||
"<li tabindex='0' class='list-group-item draggable"+(checked?' checkedItem':'')+"' id='item_"+itemID+"' data-itemid='"+itemID+"' style='height: 100%; min-width: 200px;'>" +
|
||||
" <div style='display: flex;'>" + // draggable='true'
|
||||
//" <span style='float: left; margin-right: 0px; width: 32px; margin-left: -22px;'><svg xmlns='http://www.w3.org/2000/svg' fill='gray' class='bi bi-grip-vertical' viewBox='0 0 16 16' height='32' width='32'> <path d='M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z'></path></svg></span>" +
|
||||
" <div style='flex: 10px; display: none; margin-right: 5px;' class='checkItems'><input type='checkbox'"+(checked?" checked":"")+"></div>" +
|
||||
" <div style='flex: 55%;'>"+text+"</div>" +
|
||||
|
||||
html += " <div class='priceWrapper'><span class='price'>"+(price*amount).toFixed(2)+"</span>";
|
||||
html += "<img src='../icon/dash-circle.svg' alt='Remove item' class='remItem ariaButton' data-itemID='"+itemID+"' data-price='"+price+"' style='margin-right: 3px;' tabindex='0' role='button'></div>";
|
||||
" <div style='flex: 35%;' class='priceWrapper'>" +
|
||||
" <span class='price'>"+(price*amount).toFixed(2)+"</span>" +
|
||||
" <img src='../icon/dash-circle.svg' alt='Remove item' class='remItem ariaButton' data-itemID='"+itemID+"' data-price='"+price+"' style='margin-right: 3px;' tabindex='0' role='button'>" +
|
||||
" </div>" +
|
||||
" </div>" +
|
||||
|
||||
html += " <span class='itemAmountWrapper' style='float: left; padding: 0 10px;'>";
|
||||
html += " <div class='input-group itemAmountButtons' "+(this.state !== "planning" || checked?"style='display: none;'":'')+">";
|
||||
html += " <button class='btn btn-outline-danger itemAmountBtn' type='button'>-</button>";
|
||||
html += " <input class='form-control itemAmount itemAmountBtn' type='number' value='"+amount+"' min='0' max='99' aria-label='Amount of item' >";
|
||||
if(price !== 0) {
|
||||
html += " <span class='input-group-text' style='padding-left: 3px; padding-right: 0; font-size: 12px;text-align: right;'>x<span class='price'>" + price.toFixed(2) + "</span></span>";
|
||||
}
|
||||
html += " <button class='btn btn-outline-success itemAmountBtn' type='button'>+</button>";
|
||||
html += " </div>";
|
||||
html += " <div class='itemAmountText "+(amount <= 1?"oneItem":"")+"' "+(this.state !== "shopping" || !checked?"style='display: none;'":'')+">"; //
|
||||
html += " Amount: <span class='itemAmount' style='padding-left: 10px;'>"+amount+"</span>x ";
|
||||
if(price !== 0) { html += "<span class='price'>" + price.toFixed(2) + "</span>"; }
|
||||
html += " </div>";
|
||||
html += " </span>";
|
||||
" <span class='itemAmountWrapper' style='float: left; padding: 5px 10px;'>" +
|
||||
" <div class='input-group itemAmountButtons' style='"+(this.state !== "planning" || checked?"display: none;":'')+"'>" +
|
||||
// " <div class='' style='float: left;'>" +
|
||||
" <button class='btn btn-outline-danger itemAmountBtn' type='button'>-</button>" +
|
||||
" <input class='form-control itemAmount itemAmountBtn' type='number' value='"+amount+"' min='0' max='99' aria-label='Amount of item' >" +
|
||||
(price !== 0?" <span class='input-group-text' style='padding-left: 3px; padding-right: 0; font-size: 12px;text-align: right;'>x<span class='price'>" + price.toFixed(2) + "</span></span>":"") +
|
||||
" <button class='btn btn-outline-success itemAmountBtn' type='button'>+</button>" +
|
||||
// " </div>" +
|
||||
// " <button class='btn btn-secondary' style='float: right;'>Save</button>" +
|
||||
" </div>" +
|
||||
// " <div class='itemAmountText "+(amount <= 1?"oneItem":"")+"' "+(this.state !== "shopping" || !checked?"style='display: none;'":'')+">" +
|
||||
" <div class='itemAmountText "+(amount <= 1?"oneItem":"")+"' "+(amount <= 1?"style='display: none;'":'')+">" +
|
||||
" Amount: <span class='itemAmount' style='padding-left: 10px;'>"+amount+"</span>x " +
|
||||
(price !== 0?"<span class='price'>" + price.toFixed(2) + "</span>":"") +
|
||||
" </div>" +
|
||||
" </span>" +
|
||||
|
||||
html += "</li>";
|
||||
"</li>\n";
|
||||
|
||||
this.selector.find("ul.storeItems").append(html);
|
||||
this.selector.find(".emptyList").hide();
|
||||
|
@ -404,9 +404,11 @@ class Store {
|
|||
|
||||
if(newValue === 1){
|
||||
textAmountElem.addClass('oneItem');
|
||||
textAmountElem.hide();
|
||||
}
|
||||
else {
|
||||
textAmountElem.removeClass('oneItem');
|
||||
textAmountElem.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
|
||||
|
||||
/*
|
||||
Function productSearchEvent( event )
|
||||
This function is intended to run on `onKeyUp` and `onChange` events for a text-input.
|
||||
*/
|
||||
|
||||
function productSearchEvent(event){
|
||||
const itemNameElem = $(event.target);
|
||||
let search = itemNameElem.val();
|
||||
let formElem = itemNameElem.closest('form');
|
||||
if(formElem.find('.itemSearchDropdown').length === 0){
|
||||
formElem.append('<div class="itemSearchDropdown" style="display: none;"></div>');
|
||||
}
|
||||
const searchDropdown = formElem.find('.itemSearchDropdown');
|
||||
searchDropdown.hide().html('');
|
||||
|
||||
let productSearch = new ProductSearch(itemNameElem, searchDropdown);
|
||||
|
||||
if(search.length > 2){
|
||||
productSearch.search(search);
|
||||
}
|
||||
else {
|
||||
searchDropdown.hide().html('');
|
||||
}
|
||||
}
|
||||
|
||||
let apiProductSearchTimer; // used for the product search timeout
|
||||
|
||||
class ProductSearch {
|
||||
constructor(textInput, dropdownElem) {
|
||||
this.textInput = textInput;
|
||||
this.dropdownElem = dropdownElem;
|
||||
}
|
||||
|
||||
search(search){
|
||||
// apiProductSearchTimer declared in upper scope
|
||||
clearTimeout(apiProductSearchTimer);
|
||||
|
||||
apiProductSearchTimer = setTimeout(()=>{
|
||||
$.getJSON('/api/v1/product/'+search).done(json => {
|
||||
this.dropdownElem.hide().html('');
|
||||
for (const resultKey in json.data) {
|
||||
const result = json.data[resultKey];
|
||||
let searchResHtml = "";
|
||||
if(typeof result.price !== "undefined"){
|
||||
let priceAge = priceAgeFormat(result.price.age);
|
||||
searchResHtml =
|
||||
"<li data-name='"+result.name+"' data-price='"+(result.price.price || 0)+"'>" +
|
||||
" <div style='flex: 65%;'>"+result.name+"</div>" +
|
||||
" <div style='flex: 35%;'><span class='priceWrapper price' style='color: "+priceAge.color+"' title='"+priceAge.text+"\n"+(result.price.updated || '')+"'>"+(result.price.price.toFixed(2))+"</span></div>" +
|
||||
"</li>";
|
||||
}
|
||||
else {
|
||||
searchResHtml =
|
||||
"<li data-name='"+result.name+"' data-price='0'>" +
|
||||
" <div style='float: left;'>"+result.name+"</div>" +
|
||||
"</li>";
|
||||
}
|
||||
this.dropdownElem.show().append(searchResHtml);
|
||||
}
|
||||
this.dropdownElem.find('li').on('click', ev => { this.priceClick(ev); });
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
priceClick(ev){
|
||||
let clickedResult = $(ev.currentTarget);
|
||||
let prodName = clickedResult.attr('data-name');
|
||||
let prodPrice = clickedResult.attr('data-price');
|
||||
console.log(prodName, prodPrice);
|
||||
this.textInput.val(prodName)
|
||||
this.textInput.parent().parent().find('.newItemPrice').val(prodPrice);
|
||||
this.dropdownElem.hide().html('');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function priceAgeFormat(age){
|
||||
let price = {};
|
||||
|
||||
if(age <= 4) {
|
||||
price.color = "var(--bs-success)";
|
||||
price.text = "Price is updated within the last 4 days";
|
||||
}
|
||||
else if(age <= 14) {
|
||||
price.color = "var(--bs-warning)";
|
||||
price.text = "Price is updated within the last 2 weeks";
|
||||
}
|
||||
else {
|
||||
price.color = "var(--bs-danger)";
|
||||
price.text = "Price was last updated more than 2 weeks ago";
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
|
@ -20,9 +20,9 @@ class Recipe {
|
|||
|
||||
let html = "<div class='accordion-item'>"+
|
||||
" <h2 class='accordion-header' id='heading"+key+"'>"+
|
||||
" <button class='accordion-button' type='button' data-bs-toggle='collapse' data-bs-target='#collapse"+key+"' aria-expanded='true' aria-controls='collapse"+key+"'>"+recipe.name+"</button>"+
|
||||
" <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#collapse"+key+"' aria-expanded='true' aria-controls='collapse"+key+"'>"+recipe.name+"</button>"+
|
||||
" </h2>"+
|
||||
" <div id='collapse"+key+"' class='accordion-collapse collapse' aria-labelledby='heading"+key+"' data-bs-parent='#addStoreRecipe'>"+
|
||||
" <div id='collapse"+key+"' class='accordion-collapse collapse collapsed' aria-labelledby='heading"+key+"' data-bs-parent='#addStoreRecipe'>"+
|
||||
" <div class='accordion-body'>"+
|
||||
" <ul class='list-group list-group-flush' id='recipeItems"+key+"' data-recipe-name='"+recipe.name+"' data-recipe-id='"+recipe['recipe_id']+"'>";
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<footer>
|
||||
Any prices you find on this website is added by its users and are only a guideline.<br>
|
||||
<strong>PaperBag</strong> is a <a href="//svagard.no/projects/paperbag" target="_blank" rel="nofollow">Svagård</a> project<br>
|
||||
<!--<a href="https://useg.it/eirik/GroceryAssist" target="_blank" rel="nofollow">View source</a> |
|
||||
<a href="/cookies" rel="nofollow">Cookies</a> |
|
||||
|
|
|
@ -86,6 +86,68 @@ CREATE OR REPLACE TABLE `plan_store_item` (
|
|||
CONSTRAINT plan_store_item_FK FOREIGN KEY (plan_store_id) REFERENCES `plan_store`(`plan_store_id`)
|
||||
) DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
|
||||
|
||||
|
||||
## PRODUCTS ##
|
||||
CREATE OR REPLACE TABLE `product_group` (
|
||||
group_id int(11) auto_increment NOT NULL,
|
||||
name varchar(100) NOT NULL,
|
||||
PRIMARY KEY (group_id)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE TABLE `product` (
|
||||
`product_id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`product_group_id` int(11) NOT NULL DEFAULT 1,
|
||||
`name` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`product_id`, `product_group_id`),
|
||||
CONSTRAINT `product_FK` FOREIGN KEY (`product_group_id`) REFERENCES `product_group` (`group_id`)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE TABLE `product_variant` (
|
||||
`product_id` bigint(20) NOT NULL,
|
||||
`variant_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) DEFAULT NULL,
|
||||
`storebrand` tinyint(1) DEFAULT 0,
|
||||
`EAN` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`variant_id`,`product_id`),
|
||||
KEY `product_variant_FK` (`product_id`),
|
||||
CONSTRAINT `product_variant_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`product_id`)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE TABLE `product_price` (
|
||||
`product_id` bigint(20) NOT NULL,
|
||||
`date` date NOT NULL DEFAULT curdate(),
|
||||
`price` decimal(8,2) NOT NULL,
|
||||
`product_variant` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`product_id`,`date`,`product_variant`),
|
||||
CONSTRAINT `product_price_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`product_id`)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE TABLE `nutrition` (
|
||||
`nutrition_id` mediumint(9) NOT NULL AUTO_INCREMENT,
|
||||
`nutrition` varchar(100) NOT NULL,
|
||||
`parent` mediumint(9) DEFAULT NULL,
|
||||
PRIMARY KEY (`nutrition_id`),
|
||||
KEY `nutrition_FK` (`parent`),
|
||||
CONSTRAINT `nutrition_FK` FOREIGN KEY (`parent`) REFERENCES `nutrition` (`nutrition_id`)
|
||||
);
|
||||
|
||||
INSERT INTO nutrition (nutrition_id, nutrition, parent) VALUES
|
||||
(1, 'Energy', NULL),
|
||||
(2, 'Fat', NULL), (3, 'of which saturates', 2),
|
||||
(4, 'Carbohydrate', NULL), (5, 'of which sugars', 4), (6, 'of which starch', 4),
|
||||
(7, 'Protein', NULL),
|
||||
(8, 'Salt', NULL);
|
||||
|
||||
CREATE OR REPLACE TABLE `product_nutrition` (
|
||||
`product_id` bigint(20) NOT NULL,
|
||||
`variant_id` int(11) NOT NULL,
|
||||
`nutrition_id` mediumint(9) NOT NULL,
|
||||
`value` smallint(6) NOT NULL,
|
||||
`units` enum('g','ml','cups','%') NOT NULL DEFAULT 'g',
|
||||
PRIMARY KEY (`product_id`,`variant_id`,`nutrition_id`),
|
||||
KEY `product_nutrition_FK` (`nutrition_id`),
|
||||
CONSTRAINT `product_nutrition_FK` FOREIGN KEY (`nutrition_id`) REFERENCES `nutrition` (`nutrition_id`)
|
||||
);
|
||||
";
|
||||
|
||||
$recipeSQL = "
|
||||
|
|
Loading…
Reference in New Issue