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:
2025-08-07 19:23:58 +02:00
parent b282f333fd
commit 017f88846e
2 changed files with 111 additions and 16 deletions

View File

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

View File

@@ -2,22 +2,87 @@
{% 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>
</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>
<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));
} }
}) })