Add/remove items from stores ++

master
Eirik Th S 2021-04-11 02:38:24 +02:00
parent 7b2f16828b
commit 836712868e
12 changed files with 462 additions and 130 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
webdata/connection.php
temp/

View File

@ -1,7 +1,55 @@
:root {
--bg-accent: #19A6A0;
--wsTop: constant(safe-area-inset-top);
--wsTop: env(safe-area-inset-top);
--wsBottom: constant(safe-area-inset-bottom);
--wsBottom: env(safe-area-inset-bottom);
}
html {
height: 100%;
}
body {
height: 100%;
padding-bottom: var(--wsTop);
/* padding-bottom: 50px; */
background-repeat: no-repeat;
background-attachment: fixed;
background-color: var(--bg-accent);
background-image: -o-linear-gradient(to bottom left, #eee 0%, var(--bg-accent) 100%);
background-image: -webkit-linear-gradient(to bottom left, #eee 0%, var(--bg-accent) 100%);
background-image: linear-gradient(to bottom left, #eee 0%, var(--bg-accent) 100%);
}
.navbar {
padding-top: var(--wsTop);
margin-bottom: 5px;
}
.card {
margin: 5px 2px;
margin: 5px 10px;
}
#home .card {
min-height: 250px;
}
.iconWrapper {
float: right;
margin-top: -30px;
margin-right: 10px;
}
.iconWrapper img {
margin-left: 10px;
}
.iconWrapper img, .iconBtn {
cursor: pointer;
height: 20px;
width: 20px;
}
.card-columns .card {
@ -30,18 +78,10 @@ span.price {
.addItemForm input {
transition: width 0.5s;
}
.addItemForm input:not([type="submit"]):focus {
.addItemForm input:not([type="submit"],[type="image"]):focus {
width: 40%;
}
/*.addItemForm input {
width: 60%;
.addItemForm input[type="image"] {
padding-left: 5px;
padding-right: 5px;
}
.addItemForm input[type='number'] {
width: 20%;
}
.addItemForm input[type='submit'] {
width: 20%;
}*/

3
icon/dash.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dash" viewBox="0 0 16 16">
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
</svg>

After

Width:  |  Height:  |  Size: 203 B

View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

View File

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 336 B

3
icon/plus.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>

After

Width:  |  Height:  |  Size: 247 B

4
icon/x-circle.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>

After

Width:  |  Height:  |  Size: 423 B

View File

@ -12,37 +12,35 @@
<script src="/js/jquery-3.5.1.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
</head>
<body>
<body id="home">
<?php include 'webdata/navbar.php'; ?>
<h1 class="headline text-center">Grocery Assist</h1>
<div class="container" style="padding-top: 5px;">
<div class="container-sm" style="padding-top: 5px;">
<div class="card-group" style="text-align: center;">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3" style="margin: auto; max-width: 900px;"> <!-- .row-cols-lg-4 -->
<!--<div class="row row-cols-1 row-cols-sm-2" style="margin: auto; max-width: 900px;"> <!-- .row-cols-lg-4 -->
<a href='/plan'>
<div class="col mb-4" style='min-width: 100px; min-height: 200px;'>
<div class="card h-100" style="margin: auto;">
<img src="#" class="card-img-top" alt="Plan">
<div class="card-body">
<h5 class="card-title">Plan shopping</h5>
<p class="card-text"></p>
</div>
</div>
</div>
</a>
<div class="col mb-4" style='min-width: 100px; min-height: 200px;'>
<div class="card h-100" style="margin: auto;">
<img src="#" class="card-img-top" alt="Plan"></a>
<div class="card h-100" style="">
<a href='/plan'>
<img src="#" class="card-img-top" alt="Plan">
<div class="card-body">
<h5 class="card-title">Review</h5>
<h5 class="card-title">Plan shopping</h5>
<p class="card-text"></p>
</div>
</div>
</a>
</div>
</div>
<div class="card h-100" style="">
<img src="#" class="card-img-top" alt="Plan"></a>
<div class="card-body">
<h5 class="card-title">Review</h5>
<p class="card-text"></p>
</div>
</div>
<!-- </div> -->
</div>
</div>

View File

@ -9,34 +9,84 @@ header("Content-Type: application/json");
$data = [];
$returns = [];
if(!empty($_GET)){
$data = $_GET;
}
elseif(!empty($_POST)){
$data = $_POST;
}
foreach([$_GET, $_POST] as $request){
if(!empty($request)){
foreach($request as $key => $value){
if(($data[$key] = filter($value)) === false){
print_r($value);
echo "Failed to sanitize: `".$key."`: ".$value." \t-\t type: ".gettype($value)."\n";
}
}
}
}
if(!empty($data) && isset($user_id)){
if(isset($data['plan'])){
$sql = "SELECT * FROM plan_store WHERE `user_id` = '$user_id'";
$result = $db->query($sql);
while($stores = $result->fetch_assoc()){
if($result2 = $db->query("SELECT * FROM plan_store_item WHERE `plan_store_id` = '$stores[plan_store_id]'")){
$stores['items'] = [];
if($result2->num_rows > 0){
$stores['items'] = $result2->fetch_all(MYSQLI_ASSOC);
}
if($data['plan'] == 'saveStore'){
if($data['storeName'] == ""){
returns("Missing store-name value", 1);
}
if( initStore($data['storeName']) ){
returns();
}
else {
returns($db->error,1);
}
}
$returns[] = $stores;
else if($data['plan'] == 'addItem'){
if(($temp = checkArgs(array("storeID"=>@$data['storeID'], "name"=>@$data['name'], "price"=>@$data['price']))) !== true){
returns("Missing a value: $temp", 1);
}
if( addItem($data['storeID'], $data['name'], $data['price']) ){
returns();
}
else {
returns($db->error,1);
}
}
else if($data['plan'] == "remItem"){
if(($temp = checkArgs(array("storeID"=>@$data['storeID'], "position"=>@$data['position'], "price"=>@$data['price']))) !== true){
returns("Missing a value: $temp", 1);
}
if( remItem($data['storeID'], $data['position'], $data['price']) ){
returns();
}
else {
$error = $db->error;
if($error == ""){ $error = "No rows deleted"; }
returns($error,1);
}
}
else {
$sql = "SELECT * FROM plan_store WHERE `user_id` = '$user_id'";
$result = $db->query($sql);
while($stores = $result->fetch_assoc()){
if($result2 = $db->query("SELECT * FROM plan_store_item WHERE `plan_store_id` = '$stores[plan_store_id]'")){
$stores['items'] = [];
if($result2->num_rows > 0){
$stores['items'] = $result2->fetch_all(MYSQLI_ASSOC);
}
}
else {
returns($db->error,1);
}
$returns[] = $stores;
}
}
returns($returns);
@ -46,8 +96,11 @@ else {
returns("Nothing to do", 404);
}
function returns($content, $code = 0){
if($code != 0){
returns("Fatal error!\n\nInput data:\n".print_r($data, true), 400);
function returns($content = 'Success', $code = 0){
if($code != 0 || $content == 'Success'){
$returns['status'] = $code;
$returns['message'] = $content;
}
@ -58,4 +111,99 @@ function returns($content, $code = 0){
echo json_encode($returns);
die();
}
function filter($data){
global $db;
if(gettype($data) !== "array"){
$data = trim(htmlentities(strip_tags($data)));
return $db->real_escape_string($data);
}
else if(gettype($data) == "array"){
foreach($data as $key => $value){
$data[$key] = filter($value);
}
return $data;
}
return false;
}
// class Store {
// private $storeID;
// private $storeName;
// private $items = [];
// function __construct($existing = false){
// if($existing){
// $sql = "INSERT INTO "
// }
// }
// }
function checkArgs($args){
foreach($args as $key => $arg){
if($arg == ""){
return $key;
}
}
return true;
}
function initStore($storeName){
global $db, $user_id;
// CHECK IF STORE EXISTS
$storeCheckSql = "SELECT count(0) FROM plan_store WHERE `user_id` = '$user_id' AND `name` = '$storeName';";
if($storeCheckRes = $db->query($storeCheckSql)){
$matchingStores = $storeCheckRes->fetch_row()[0];
if($matchingStores == 1){
$sql = "UPDATE plan_store SET null WHERE `user_id` = '$user_id' AND `name` = '$storeName';";
}
else if($matchingStores == 0){
$sql = "INSERT INTO plan_store (user_id, `name`) VALUES ($user_id, '$storeName');";
}
else {
return false;
}
if( $db->query($sql) !== false){
return true;
}
}
return false;
}
function addItem($storeID, $name, $price){
global $db, $user_id;
$insertItemSQL = "INSERT INTO plan_store_item (`plan_store_id`, `name`, `price`) VALUES ((SELECT plan_store_id FROM plan_store WHERE `user_id` = '$user_id' AND `plan_store_id` = '$storeID'), '$name', $price);";
if($db->query($insertItemSQL)){
return true;
}
return false;
}
function remItem($storeID, $pos, $price){
global $db, $user_id;
$verifyUserOwnershipSQL = "SELECT `plan_store_id` FROM plan_store WHERE `user_id` = '$user_id' AND `plan_store_id` = '$storeID'";
$findRowSql = "SELECT `plan_item_id` FROM plan_store_item WHERE `plan_store_id` = ($verifyUserOwnershipSQL) LIMIT $pos, 1";
$removeItemsql = "DELETE FROM plan_store_item WHERE `plan_item_id` = ($findRowSql) AND `price` = '$price';";
if($db->query($removeItemsql)){
if(mysqli_affected_rows($db) > 0){
return true;
}
return false;
}
return false;
}
?>

View File

@ -10,43 +10,28 @@
<link rel="stylesheet" href="/css/index.css" type="text/css" />
<script src="/js/jquery-3.5.1.min.js"></script>
<script src="//unpkg.com/@popperjs/core@2"></script>
<script src="/js/bootstrap.min.js"></script>
</head>
<body>
<body id='plan'>
<?php include '../webdata/navbar.php'; ?>
<h1 class="headline text-center">Grocery Assist</h1>
<div class="container" style="padding-top: 5px; text-align: center;">
<div class="card-columns" id="stores" style="text-align: center;">
<div class="" style="margin: auto; max-width: 900px;">
<?php
for ($i=0; $i < 0; $i++) {
?>
<div class="card store" id="store<?=$i+1;?>" style="width: 18rem;">
<div class='card-header'>Store <?=$i+1;?></div>
<div class="card-body">
<ul class="list-group list-group-flush">
<?php for ($j=0; $j < random_int(1,10); $j++) {
echo "<li class='list-group-item'><span>Item ".($j+1)."</span><span class='price'>".random_int(9,200).",-</span></li>";
} ?>
</ul>
</div>
<div class='card-footer subtotal'>Subtotal: <span class='price'>-,-</span></div>
</div>
<?php
}
?>
</div>
<hr>
<h5>Please enable javascript for this page to work</h5>
<hr>
</div>
<button id="addStore">Add store</button>
<br>
<button id="addStore">Add store</button><br><br>
<button id="refreshAll">Refresh all</button>
</div>
<script src='plan.js'></script>
</body>
</html>

View File

@ -1,29 +1,36 @@
/*jshint sub:true, esversion: 6, -W083 */
class Store {
constructor(title) {
constructor(title, storeID) {
this.items = [];
title = title || "Store "+($(".store").length+1);
let storeNum = $(".store").length+1;
this.title = title || "Store "+storeNum;
this.storeID = storeID || null;
let html = "";
html += "<div class='card store' style='width: 20rem;'>";
html += " <div class='card-header'>"+title+"</div>";
html += " <div class='card-body'>";
html += " <ul class='list-group list-group-flush'>";
html += " <li class='list-group-item emptyList'>No items added</li>";
html += " </ul>";
html += " <hr>";
html += " <form action='#!' class='form-row input-group input-group-sm addItemForm'>";
html += " <input type='text' class='form-control newItemName'>";
html += " <input type='number' class='form-control newItemPrice' value='0' min='0'>";
html += " <div class='input-group-append'>";
html += " <input type='submit' class='form-control addItem' value='+'>";
html += " </div>";
html += " </form>";
html += " </div>";
html += " <div class='card-footer subtotal'>Subtotal: <span class='price'>-,-</span></div>";
html += " <div class='card-header'>"+this.title+"</div>";
html += " <div class='iconWrapper'>";
html += " <img src='/icon/pencil-square.svg' class='editStoreName' alt='edit name' data-toggle='tooltip' title='Edit store name' tabindex=0 role='button' />";
html += " <img src='/icon/x-circle.svg' class='removeStore' alt='remove store' data-toggle='tooltip' title='Remove store' tabindex=0 role='button' />";
html += " </div>";
html += " <div class='card-body'>";
html += " <ul class='list-group list-group-flush'>";
html += " <li class='list-group-item emptyList'>No items added</li>";
html += " </ul>";
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='input-group-append'>";
html += " <input type='image' class='form-control addItem' src='/icon/plus.svg' alt='+'>";
html += " </div>";
html += " </form>";
html += " <button class='save'>Save</button>";
html += " </div>";
html += " <div class='card-footer subtotal'>Subtotal: <span class='price'>0.00 kr</span></div>";
html += "</div>";
this.selector = $(html).appendTo("#stores");
@ -32,36 +39,97 @@ class Store {
this.selector.find(".addItemForm").on('submit', function(ev){
ev.preventDefault();
if( that.addItem($(this).find('.newItemName').val(), $(this).find('.newItemPrice').val()) !== false){
that.addItem($(this).find('.newItemName').val(), $(this).find('.newItemPrice').val())
.done(json => {
$(this).find('.newItemPrice').val(0);
$(this).find('.newItemName').val("").focus();
});
});
this.selector.find('.editStoreName').on('click keyup', ev => {
if(ev.type == 'click' || ev.keyCode == 13){
// $(ev.target).tooltip('hide').css('background-color','red');
alert("Coming soon");
}
});
this.selector.find('.removeStore').on('click keyup', ev => {
if(ev.type == 'click' || ev.keyCode == 13){
if(confirm("Are you sure you want to remove this store?")){ this.removeStore(); }
}
});
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
/** DEV **/
this.selector.find('.save').click(ev => { this.save(); });
}
addItem(text, price){
if(text.length > 0){
try {
price = Number(price);
this.items.push({ text: text, price: price });
this.selector.find("ul").append("<li class='list-group-item'><span>"+text+"</span><span class='price'>"+price+",- <button class='remItem'>-</button></span></li>");
this.selector.find(".emptyList").hide();
this.verify();
return true;
}
catch(e){
alert("Something failed. Try again.");
console.error(e, e.stack);
}
let that = this;
return $.ajax({
method: "POST",
url: "do.php",
data: { plan: 'addItem', storeID: this.storeID, name: text, price: price },
dataType: 'JSON'
})
.done(json => {
if(handleJsonErrors(json)){
return false;
}
// console.log("addItem return:", json);
return that.addItemHtml(text, price);
})
.fail(handleAjaxErrors);
}
return false;
}
remItem(pos){
addItemHtml(text, price){
try {
price = Number(price);
this.items.push({ text: text, price: price });
this.selector.find("ul").append("<li class='list-group-item'><span>"+text+"</span><span class='price'>"+price.toFixed(2)+" kr <button class='remItem' data-price='"+price+"' data-toggle='tooltip' title='Remove item'><img class='iconBtn' src='/icon/dash.svg' alt='-' style='height: 16px; width: 16px;' /></button></span></li>");
this.selector.find(".emptyList").hide();
this.verify();
return true;
}
catch(e){
alert("Something failed. Try again.");
console.error(e, e.stack);
}
return false;
}
remItem(pos, price){
let that = this;
return $.ajax({
method: "POST",
url: "do.php",
data: { plan: 'remItem', storeID: this.storeID, position: pos, price: price },
dataType: 'JSON'
})
.done(json => {
if(handleJsonErrors(json)){
return false;
}
// console.log("remItem return:", json);
return that.remItemHtml(pos);
})
.fail(handleAjaxErrors);
}
remItemHtml(pos){
this.items.splice(pos,1);
// remove from database here?
$(this.selector).find(".remItem").unbind().each((key, val) => {
if(key == pos){
@ -70,6 +138,7 @@ class Store {
});
this.verify();
return true;
}
verify(){
// UPDATE TOTAL PRICE
@ -78,7 +147,7 @@ class Store {
total += item.price;
});
$(this.selector).find(".subtotal .price").html(total+",-");
$(this.selector).find(".subtotal .price").html(total.toFixed(2)+" kr");
// SHOW if-empty MESSAGE
if(this.selector.find("li").length <= 1){
@ -88,9 +157,38 @@ class Store {
// BIND remove-buttons
$(this.selector).find(".remItem").unbind().each((key, val) => {
let that = this;
$(val).click(function(){ that.remItem(key); });
$(val).click(function(){
that.remItem(key, $(this).attr("data-price"));
try {
$(this).tooltip('dispose');
}
catch(ignore){}
});
});
}
save(){
$.ajax({
method: "POST",
url: "do.php",
data: { plan: 'saveStore', storeName: this.title, items: this.items },
dataType: 'JSON'
})
.done(json => {
// json = JSON.parse(body);
if(handleJsonErrors(json)){
return;
}
console.log(json);
})
.fail(handleAjaxErrors);
}
removeStore(){
this.selector.remove();
}
}
@ -100,22 +198,50 @@ $("#addStore").click(ev => {
stores.push(new Store());
});
// ADD STORE
$("#refreshAll").click(ev => {
stores = [];
getStores();
});
// $(document).on('ready', function(){
$(".store").each((_1, store) => {
// console.log(key, value);
let totalPrice = 0;
$(store).find("li span.price").each((_2,item) => {
// GET STORES
function getStores(){
$("#stores").html("");
let priceStr = item.innerHTML;
try {
totalPrice += Number(priceStr.substr(0, priceStr.length-2));
$.getJSON('do.php', { plan: '' })
.done(json => {
if(handleJsonErrors(json)){
return;
}
// console.log(json);
for(const store in json.data){
storeID = stores.length;
stores.push(new Store(json.data[store].name, json.data[store].plan_store_id));
for(const item in json.data[store].items){
stores[storeID].addItemHtml(json.data[store].items[item].name, json.data[store].items[item].price);
}
catch(e){
console.error("Failed find the number in '"+priceStr+"': "+e);
}
});
$(store).find(".subtotal .price").html(totalPrice+",-");
});
// });
}
})
.fail(handleAjaxErrors);
}
getStores();
function handleJsonErrors(json){
if(typeof json.status != "undefined" && json.status != 0){
alert(json.message);
return true;
}
return false;
}
function handleAjaxErrors(jqxhr, textStatus, error){
if(textStatus === "parsererror" && jqxhr.responseText != ""){
alert("An error occured:\n"+jqxhr.responseText);
}
else {
alert("An error occured:\n"+error);
}
}

24
webdata/navbar.php Normal file
View File

@ -0,0 +1,24 @@
<?php $activePage = $_SERVER['REQUEST_URI']; if($activePage == "/index.php"){ $activePage = "/"; } ?><nav class="navbar navbar-expand-sm navbar-light bg-light" style="z-index: 2;">
<div class="container-fluid" style="max-width: 900px;">
<a class="navbar-brand" href="/"><img src='#' style="height: 24px;" alt='Groceries' /></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-sm-0">
<?php
$links["Home"] = "/";
$links["Plan"] = "/plan/";
$links["Review"] = "/review/";
foreach($links as $title => $href){
echo '<li class="nav-item">';
echo '<a class="nav-link'.($activePage == $href?" active\" aria-current=\"page":"").'" href="'.$href.'">'.$title.'</a>';
echo "</li>\n";
}
?>
</ul>
</div>
</div>
</nav>