fix add item formatting and suggestion loigic

This commit is contained in:
Nico 2026-01-02 17:00:48 -08:00
parent aa9e71194c
commit 0d5316bc27
6 changed files with 268 additions and 49 deletions

View File

@ -1,7 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import "../../styles/components/AddItemForm.css";
import SuggestionList from "../items/SuggestionList"; import SuggestionList from "../items/SuggestionList";
export default function AddItemForm({ onAdd, onSuggest, suggestions, buttonText = "Add Item" }) { export default function AddItemForm({ onAdd, onSuggest, suggestions, buttonText = "Add" }) {
const [itemName, setItemName] = useState(""); const [itemName, setItemName] = useState("");
const [quantity, setQuantity] = useState(1); const [quantity, setQuantity] = useState(1);
const [showSuggestions, setShowSuggestions] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false);
@ -23,38 +24,76 @@ export default function AddItemForm({ onAdd, onSuggest, suggestions, buttonText
const handleSuggestionSelect = (suggestion) => { const handleSuggestionSelect = (suggestion) => {
setItemName(suggestion); setItemName(suggestion);
setShowSuggestions(false); setShowSuggestions(false);
onSuggest(suggestion); // Trigger button text update
}; };
const incrementQuantity = () => {
setQuantity(prev => prev + 1);
};
const decrementQuantity = () => {
setQuantity(prev => Math.max(1, prev - 1));
};
const isDisabled = !itemName.trim();
return ( return (
<form onSubmit={handleSubmit}> <div className="add-item-form-container">
<input <form onSubmit={handleSubmit} className="add-item-form">
type="text" <div className="add-item-form-field">
className="glist-input" <input
placeholder="Item name" type="text"
value={itemName} className="add-item-form-input"
onChange={(e) => handleInputChange(e.target.value)} placeholder="Enter item name"
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)} value={itemName}
onClick={() => setShowSuggestions(true)} onChange={(e) => handleInputChange(e.target.value)}
/> onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
onClick={() => setShowSuggestions(true)}
/>
{showSuggestions && suggestions.length > 0 && ( {showSuggestions && suggestions.length > 0 && (
<SuggestionList <SuggestionList
suggestions={suggestions} suggestions={suggestions}
onSelect={handleSuggestionSelect} onSelect={handleSuggestionSelect}
/> />
)} )}
</div>
<input <div className="add-item-form-actions">
type="number" <div className="add-item-form-quantity-control">
min="1" <button
className="glist-input" type="button"
value={quantity} className="quantity-btn quantity-btn-minus"
onChange={(e) => setQuantity(Number(e.target.value))} onClick={decrementQuantity}
/> disabled={quantity <= 1}
>
</button>
<input
type="number"
min="1"
className="add-item-form-quantity-input"
value={quantity}
readOnly
/>
<button
type="button"
className="quantity-btn quantity-btn-plus"
onClick={incrementQuantity}
>
+
</button>
</div>
<button type="submit" className="glist-btn"> <button
{buttonText} type="submit"
</button> className={`add-item-form-submit ${isDisabled ? 'disabled' : ''}`}
</form> disabled={isDisabled}
>
{buttonText}
</button>
</div>
</form>
</div>
); );
} }

View File

@ -1,3 +1,5 @@
import React from "react";
interface Props { interface Props {
suggestions: string[]; suggestions: string[];
onSelect: (value: string) => void; onSelect: (value: string) => void;
@ -8,15 +10,12 @@ export default function SuggestionList({ suggestions, onSelect }: Props) {
return ( return (
<ul <ul
className="suggestion-list"
style={{ style={{
background: "#fff", background: "#fff",
border: "1px solid #ccc", border: "1px solid #ccc",
maxHeight: "150px", maxHeight: "150px",
overflowY: "auto", overflowY: "auto",
position: "absolute",
zIndex: 1000,
left: "1em",
right: "1em",
listStyle: "none", listStyle: "none",
padding: 0, padding: 0,
margin: 0, margin: 0,

View File

@ -21,7 +21,7 @@ export default function GroceryList() {
const [sortedItems, setSortedItems] = useState([]); const [sortedItems, setSortedItems] = useState([]);
const [sortMode, setSortMode] = useState("zone"); const [sortMode, setSortMode] = useState("zone");
const [suggestions, setSuggestions] = useState([]); const [suggestions, setSuggestions] = useState([]);
const [showAddForm, setShowAddForm] = useState(false); const [showAddForm, setShowAddForm] = useState(true);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [buttonText, setButtonText] = useState("Add Item"); const [buttonText, setButtonText] = useState("Add Item");
const [pendingItem, setPendingItem] = useState(null); const [pendingItem, setPendingItem] = useState(null);
@ -103,16 +103,9 @@ export default function GroceryList() {
const exactMatch = allItems.find(item => item.item_name.toLowerCase() === lowerText); const exactMatch = allItems.find(item => item.item_name.toLowerCase() === lowerText);
if (exactMatch) { if (exactMatch) {
setButtonText("Add Item"); setButtonText("Add");
} else { } else {
// Check for similar items (80% match) setButtonText("Create + Add");
const similar = findSimilarItems(text, allItems, 80);
if (similar.length > 0) {
// Show suggestion in button but allow creation
setButtonText("Create and Add Item");
} else {
setButtonText("Create and Add Item");
}
} }
try { try {
@ -129,10 +122,23 @@ export default function GroceryList() {
const lowerItemName = itemName.toLowerCase().trim(); const lowerItemName = itemName.toLowerCase().trim();
// Combine both unbought and recently bought items for similarity checking // First check if exact item exists in database (case-insensitive)
const allItems = [...items, ...recentlyBoughtItems]; let existingItem = null;
try {
const response = await getItemByName(itemName);
existingItem = response.data;
} catch {
existingItem = null;
}
// Check for 80% similar items // If exact item exists, skip similarity check and process directly
if (existingItem) {
await processItemAddition(itemName, quantity);
return;
}
// Only check for similar items if exact item doesn't exist
const allItems = [...items, ...recentlyBoughtItems];
const similar = findSimilarItems(itemName, allItems, 80); const similar = findSimilarItems(itemName, allItems, 80);
if (similar.length > 0) { if (similar.length > 0) {
// Show modal and wait for user decision // Show modal and wait for user decision
@ -141,7 +147,7 @@ export default function GroceryList() {
return; return;
} }
// Continue with normal flow // Continue with normal flow for new items
await processItemAddition(itemName, quantity); await processItemAddition(itemName, quantity);
}; };
@ -327,7 +333,6 @@ export default function GroceryList() {
<div className="glist-container"> <div className="glist-container">
<h1 className="glist-title">Costco Grocery List</h1> <h1 className="glist-title">Costco Grocery List</h1>
<SortDropdown value={sortMode} onChange={setSortMode} />
{[ROLES.ADMIN, ROLES.EDITOR].includes(role) && showAddForm && ( {[ROLES.ADMIN, ROLES.EDITOR].includes(role) && showAddForm && (
<AddItemForm <AddItemForm
@ -338,6 +343,8 @@ export default function GroceryList() {
/> />
)} )}
<SortDropdown value={sortMode} onChange={setSortMode} />
{sortMode === "zone" ? ( {sortMode === "zone" ? (
// Grouped view by zone // Grouped view by zone
(() => { (() => {

View File

@ -0,0 +1,172 @@
/* Add Item Form Container */
.add-item-form-container {
background: var(--color-bg-surface);
padding: var(--spacing-lg);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-md);
margin-bottom: var(--spacing-xs);
border: var(--border-width-thin) solid var(--color-border-light);
}
.add-item-form {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
/* Form Fields */
.add-item-form-field {
display: flex;
flex-direction: column;
position: relative;
}
.add-item-form-input {
padding: var(--input-padding-y) var(--input-padding-x);
border: var(--border-width-thin) solid var(--input-border-color);
border-radius: var(--input-border-radius);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
transition: var(--transition-base);
width: 100%;
}
.add-item-form-input:focus {
outline: none;
border-color: var(--input-focus-border-color);
box-shadow: var(--input-focus-shadow);
}
/* Suggestion List Positioning */
.add-item-form-field .suggestion-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: var(--spacing-xs);
z-index: var(--z-dropdown);
}
/* Actions Row */
.add-item-form-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-md);
}
/* Quantity Control */
.add-item-form-quantity-control {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.quantity-btn {
width: 40px;
height: 40px;
border: var(--border-width-thin) solid var(--color-border-medium);
background: var(--color-bg-surface);
color: var(--color-text-primary);
border-radius: var(--border-radius-sm);
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
cursor: pointer;
transition: var(--transition-base);
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.quantity-btn:hover:not(:disabled) {
background: var(--color-primary-light);
border-color: var(--color-primary);
color: var(--color-primary);
}
.quantity-btn:active:not(:disabled) {
transform: scale(0.95);
}
.quantity-btn:disabled {
background: var(--color-bg-disabled);
color: var(--color-text-disabled);
border-color: var(--color-border-disabled);
cursor: not-allowed;
opacity: 0.5;
}
.add-item-form-quantity-input {
width: 40px;
max-width: 40px;
padding: var(--input-padding-y) var(--input-padding-x);
border: var(--border-width-thin) solid var(--input-border-color);
border-radius: var(--input-border-radius);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
text-align: center;
transition: var(--transition-base);
-moz-appearance: textfield; /* Remove spinner in Firefox */
}
/* Remove spinner arrows in Chrome/Safari */
.add-item-form-quantity-input::-webkit-outer-spin-button,
.add-item-form-quantity-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.add-item-form-quantity-input:focus {
outline: none;
border-color: var(--input-focus-border-color);
box-shadow: var(--input-focus-shadow);
}
/* Submit Button */
.add-item-form-submit {
height: 40px;
padding: 0 var(--spacing-lg);
background: var(--color-primary);
color: var(--color-text-inverse);
border: none;
border-radius: var(--button-border-radius);
font-size: var(--font-size-base);
font-weight: var(--button-font-weight);
cursor: pointer;
transition: var(--transition-base);
margin-top: var(--spacing-sm);
}
.add-item-form-submit:hover:not(:disabled) {
background: var(--color-primary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.add-item-form-submit:active:not(:disabled) {
transform: translateY(0);
}
.add-item-form-submit.disabled,
.add-item-form-submit:disabled {
background: var(--color-bg-disabled);
color: var(--color-text-disabled);
cursor: not-allowed;
opacity: 0.6;
box-shadow: none;
transform: none;
}
/* Responsive */
@media (max-width: 480px) {
.add-item-form-container {
padding: var(--spacing-md);
}
.quantity-btn {
width: 36px;
height: 36px;
font-size: var(--font-size-lg);
}
}

View File

@ -1,7 +1,7 @@
/* Container */ /* Container */
.glist-body { .glist-body {
font-family: var(--font-family-base); font-family: var(--font-family-base);
padding: var(--spacing-md); padding: var(--spacing-sm);
background: var(--color-bg-body); background: var(--color-bg-body);
} }
@ -9,7 +9,7 @@
max-width: var(--container-max-width); max-width: var(--container-max-width);
margin: auto; margin: auto;
background: var(--color-bg-surface); background: var(--color-bg-surface);
padding: var(--spacing-md); padding: var(--spacing-sm);
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-card); box-shadow: var(--shadow-card);
} }

View File

@ -60,6 +60,7 @@
--color-text-secondary: #6c757d; --color-text-secondary: #6c757d;
--color-text-muted: #adb5bd; --color-text-muted: #adb5bd;
--color-text-inverse: #ffffff; --color-text-inverse: #ffffff;
--color-text-disabled: #6c757d;
/* Background Colors */ /* Background Colors */
--color-bg-body: #f8f9fa; --color-bg-body: #f8f9fa;
@ -71,6 +72,7 @@
--color-border-light: #e0e0e0; --color-border-light: #e0e0e0;
--color-border-medium: #ccc; --color-border-medium: #ccc;
--color-border-dark: #999; --color-border-dark: #999;
--color-border-disabled: #dee2e6;
/* ============================================ /* ============================================
SPACING SPACING