fix add item formatting and suggestion loigic
This commit is contained in:
parent
aa9e71194c
commit
0d5316bc27
@ -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,14 +24,27 @@ 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">
|
||||||
|
<form onSubmit={handleSubmit} className="add-item-form">
|
||||||
|
<div className="add-item-form-field">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="glist-input"
|
className="add-item-form-input"
|
||||||
placeholder="Item name"
|
placeholder="Enter item name"
|
||||||
value={itemName}
|
value={itemName}
|
||||||
onChange={(e) => handleInputChange(e.target.value)}
|
onChange={(e) => handleInputChange(e.target.value)}
|
||||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
||||||
@ -43,18 +57,43 @@ export default function AddItemForm({ onAdd, onSuggest, suggestions, buttonText
|
|||||||
onSelect={handleSuggestionSelect}
|
onSelect={handleSuggestionSelect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="add-item-form-actions">
|
||||||
|
<div className="add-item-form-quantity-control">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="quantity-btn quantity-btn-minus"
|
||||||
|
onClick={decrementQuantity}
|
||||||
|
disabled={quantity <= 1}
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
className="glist-input"
|
className="add-item-form-quantity-input"
|
||||||
value={quantity}
|
value={quantity}
|
||||||
onChange={(e) => setQuantity(Number(e.target.value))}
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="quantity-btn quantity-btn-plus"
|
||||||
|
onClick={incrementQuantity}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" className="glist-btn">
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`add-item-form-submit ${isDisabled ? 'disabled' : ''}`}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
(() => {
|
(() => {
|
||||||
|
|||||||
172
frontend/src/styles/components/AddItemForm.css
Normal file
172
frontend/src/styles/components/AddItemForm.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user