mirror of
https://github.com/StefBuwalda/cal_counter.git
synced 2025-10-30 11:19:59 +00:00
Add search and autocomplete to barcode scan page
Enhanced the barcode scanner page with a search box and autocomplete suggestions for food items. Updated backend routes to support searching by both barcode and name, and added a new /query endpoint for AJAX search. Refactored step3 and step3_alt1 routes to handle both barcode and name inputs.
This commit is contained in:
@@ -4,11 +4,14 @@ from flask import (
|
|||||||
url_for,
|
url_for,
|
||||||
render_template,
|
render_template,
|
||||||
session,
|
session,
|
||||||
|
request,
|
||||||
|
jsonify,
|
||||||
)
|
)
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from forms import FoodItemForm, FoodLogForm
|
from forms import FoodItemForm, FoodLogForm
|
||||||
from application import db
|
from application import db
|
||||||
from models import FoodItem, FoodLog
|
from models import FoodItem, FoodLog
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
bp = Blueprint(
|
bp = Blueprint(
|
||||||
"add_meal",
|
"add_meal",
|
||||||
@@ -37,22 +40,32 @@ def step2():
|
|||||||
return render_template("scan_barcode.html")
|
return render_template("scan_barcode.html")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/step3/<barcode>", methods=["GET"])
|
@bp.route("/step3/<string:input>", methods=["GET"])
|
||||||
def step3(barcode: str):
|
def step3(input: str):
|
||||||
|
# check if meal_type cookie is set
|
||||||
if "meal_type" not in session:
|
if "meal_type" not in session:
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
assert barcode.isdigit()
|
|
||||||
item = current_user.food_items.filter_by(barcode=barcode).first()
|
# Check if input is a barcode
|
||||||
|
if input.isdigit():
|
||||||
|
item = current_user.food_items.filter_by(barcode=input).first()
|
||||||
if item is None:
|
if item is None:
|
||||||
# Does not exist, add item
|
# Does not exist, add item
|
||||||
return redirect(url_for("add_meal.step3_alt1", barcode=barcode))
|
return redirect(url_for("add_meal.step3_alt1", input=input))
|
||||||
else:
|
else:
|
||||||
|
# input is not a number, must be the name of the item.
|
||||||
|
item = current_user.food_items.filter_by(name=input).first()
|
||||||
|
if item is None:
|
||||||
|
# Does not exist, add manually.
|
||||||
|
return redirect(url_for("add_meal.step3_alt1", input=input))
|
||||||
|
|
||||||
|
# Track item to add and continue to next step
|
||||||
session["item_id"] = item.id
|
session["item_id"] = item.id
|
||||||
return redirect(url_for("add_meal.step4"))
|
return redirect(url_for("add_meal.step4"))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/step3_alt1/<barcode>", methods=["GET", "POST"])
|
@bp.route("/step3_alt1/<string:input>", methods=["GET", "POST"])
|
||||||
def step3_alt1(barcode: str):
|
def step3_alt1(input: str):
|
||||||
form = FoodItemForm()
|
form = FoodItemForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
print("[DEBUG] Valid form")
|
print("[DEBUG] Valid form")
|
||||||
@@ -89,8 +102,10 @@ def step3_alt1(barcode: str):
|
|||||||
print("[DEBUG] New item added")
|
print("[DEBUG] New item added")
|
||||||
return redirect(url_for("add_meal.step3", barcode=form.barcode.data))
|
return redirect(url_for("add_meal.step3", barcode=form.barcode.data))
|
||||||
print("[DEBUG] Invalid form")
|
print("[DEBUG] Invalid form")
|
||||||
if barcode.isdigit():
|
if input.isdigit():
|
||||||
form.barcode.data = barcode
|
form.barcode.data = input
|
||||||
|
else:
|
||||||
|
form.name.data = input
|
||||||
return render_template("add_item.html", form=form)
|
return render_template("add_item.html", form=form)
|
||||||
|
|
||||||
|
|
||||||
@@ -129,3 +144,18 @@ def step4():
|
|||||||
case _:
|
case _:
|
||||||
tod = "Unknown"
|
tod = "Unknown"
|
||||||
return render_template("step4.html", tod=tod, item=item, form=form)
|
return render_template("step4.html", tod=tod, item=item, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/query", methods=["GET"])
|
||||||
|
def query():
|
||||||
|
q = request.args.get("q", "").strip().lower()
|
||||||
|
if not q:
|
||||||
|
return jsonify([])
|
||||||
|
|
||||||
|
words = q.split()
|
||||||
|
filters = [
|
||||||
|
FoodItem.name.ilike(f"%{word}%") for word in words # type: ignore
|
||||||
|
]
|
||||||
|
|
||||||
|
results = current_user.food_items.filter(and_(*filters)).all()
|
||||||
|
return jsonify([item.name for item in results])
|
||||||
|
|||||||
@@ -2,23 +2,88 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
|
<!-- Header -->
|
||||||
<div class="text-center mb-4">
|
<div class="text-center mb-4">
|
||||||
<h1 class="fw-bold">Barcode Scanner</h1>
|
<h1 class="fw-bold">Barcode Scanner</h1>
|
||||||
<p class="text-muted">Use your camera to scan barcodes</p>
|
<p class="text-muted">Use your camera to scan barcodes</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Video preview -->
|
||||||
<div class="d-flex justify-content-center mb-4">
|
<div class="d-flex justify-content-center mb-4">
|
||||||
<video id="video" class="border rounded shadow-sm" style="width: 100%; max-width: 500px;" autoplay
|
<video id="video" class="border rounded shadow-sm" style="width: 100%; max-width: 500px;" autoplay
|
||||||
muted></video>
|
muted></video>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-center">
|
<!-- Start/Stop buttons -->
|
||||||
|
<div class="d-flex justify-content-center gap-3 mb-4">
|
||||||
<button id="startButton" class="btn btn-primary px-4">Start Scanning</button>
|
<button id="startButton" class="btn btn-primary px-4">Start Scanning</button>
|
||||||
<button id="stopButton" class="btn btn-danger px-4 ms-3">Stop</button>
|
<button id="stopButton" class="btn btn-danger px-4">Stop</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search box and suggestions -->
|
||||||
|
<div class="d-flex justify-content-center mt-4">
|
||||||
|
<div class="input-group" style="max-width: 500px; width: 100%;">
|
||||||
|
<input type="text" id="search-box" class="form-control" placeholder="Search..." autocomplete="off">
|
||||||
|
<button id="go-button" class="btn btn-success">Go</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Suggestions list -->
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<ul id="suggestions" class="list-group position-absolute mt-1 w-100" style="max-width: 500px; z-index: 1000;">
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const searchBox = document.getElementById('search-box');
|
||||||
|
const suggestionsBox = document.getElementById('suggestions');
|
||||||
|
const goButton = document.getElementById('go-button');
|
||||||
|
|
||||||
|
searchBox.addEventListener('input', function () {
|
||||||
|
const query = searchBox.value;
|
||||||
|
if (query.length < 2) {
|
||||||
|
suggestionsBox.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`{{url_for("add_meal.query")}}?q=${encodeURIComponent(query)}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
suggestionsBox.innerHTML = '';
|
||||||
|
data.forEach(item => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = item;
|
||||||
|
|
||||||
|
// Apply Bootstrap classes
|
||||||
|
li.classList.add('list-group-item', 'list-group-item-action', 'cursor-pointer');
|
||||||
|
|
||||||
|
// Add click behavior
|
||||||
|
li.addEventListener('click', () => {
|
||||||
|
searchBox.value = item;
|
||||||
|
suggestionsBox.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to suggestions box
|
||||||
|
suggestionsBox.appendChild(li);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Redirect when the "Go" button is clicked
|
||||||
|
goButton.addEventListener('click', () => {
|
||||||
|
const value = searchBox.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const baseURL = "{{url_for('add_meal.step3', input='!')}}";
|
||||||
|
window.location.href = baseURL.replace("!", encodeURIComponent(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { BrowserMultiFormatReader } from 'https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/+esm';
|
import { BrowserMultiFormatReader } from 'https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/+esm';
|
||||||
|
|
||||||
@@ -49,7 +114,7 @@
|
|||||||
if (result) {
|
if (result) {
|
||||||
// Result found, this should post the barcode
|
// Result found, this should post the barcode
|
||||||
const codeText = result.getText();
|
const codeText = result.getText();
|
||||||
const baseURL = "{{url_for('add_meal.step3', barcode='!')}}";
|
const baseURL = "{{url_for('add_meal.step3', input='!')}}";
|
||||||
window.location.href = baseURL.replace("!", encodeURIComponent(codeText));
|
window.location.href = baseURL.replace("!", encodeURIComponent(codeText));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user