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
Eirik Th S 2022-01-26 17:22:56 +01:00
parent 02bd4d4131
commit 7d57b449b4
11 changed files with 503 additions and 54 deletions

View File

@ -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]

View File

@ -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)){

154
www/api/v1/products.php Normal file
View File

@ -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;
}

View File

@ -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 ? **/

59
www/plan/htmlElements.js Normal file
View File

@ -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>"
}
}

View File

@ -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'; ?>

View File

@ -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();
}
}
});

96
www/plan/product.js Normal file
View File

@ -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;
}

View File

@ -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']+"'>";

View File

@ -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> |

View File

@ -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 = "