merge image and confirmbuy modal and improve on them

This commit is contained in:
Nico 2026-01-22 00:41:46 -08:00
parent ce2574c454
commit 1300cbb0a8
6 changed files with 320 additions and 52 deletions

View File

@ -1,12 +1,11 @@
import { memo, useRef, useState } from "react"; import { memo, useRef, useState } from "react";
import AddImageModal from "../modals/AddImageModal"; import AddImageModal from "../modals/AddImageModal";
import ConfirmBuyModal from "../modals/ConfirmBuyModal"; import ConfirmBuyModal from "../modals/ConfirmBuyModal";
import ImageModal from "../modals/ImageModal";
function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) { function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems = [] }) {
const [showModal, setShowModal] = useState(false);
const [showAddImageModal, setShowAddImageModal] = useState(false); const [showAddImageModal, setShowAddImageModal] = useState(false);
const [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false); const [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false);
const [currentItem, setCurrentItem] = useState(item);
const longPressTimer = useRef(null); const longPressTimer = useRef(null);
const pressStartPos = useRef({ x: 0, y: 0 }); const pressStartPos = useRef({ x: 0, y: 0 });
@ -57,13 +56,14 @@ function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) {
const handleItemClick = () => { const handleItemClick = () => {
if (onClick) { if (onClick) {
setCurrentItem(item);
setShowConfirmBuyModal(true); setShowConfirmBuyModal(true);
} }
}; };
const handleConfirmBuy = (quantity) => { const handleConfirmBuy = (quantity) => {
if (onClick) { if (onClick) {
onClick(quantity); onClick(currentItem.id, quantity);
} }
setShowConfirmBuyModal(false); setShowConfirmBuyModal(false);
}; };
@ -72,10 +72,16 @@ function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) {
setShowConfirmBuyModal(false); setShowConfirmBuyModal(false);
}; };
const handleNavigate = (newItem) => {
setCurrentItem(newItem);
};
const handleImageClick = (e) => { const handleImageClick = (e) => {
e.stopPropagation(); // Prevent triggering the bought action e.stopPropagation(); // Prevent triggering the bought action
if (item.item_image) { if (item.item_image) {
setShowModal(true); // Open buy modal which now shows the image
setCurrentItem(item);
setShowConfirmBuyModal(true);
} else { } else {
setShowAddImageModal(true); setShowAddImageModal(true);
} }
@ -150,14 +156,6 @@ function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) {
</div> </div>
</li> </li>
{showModal && (
<ImageModal
imageUrl={imageUrl}
itemName={item.item_name}
onClose={() => setShowModal(false)}
/>
)}
{showAddImageModal && ( {showAddImageModal && (
<AddImageModal <AddImageModal
itemName={item.item_name} itemName={item.item_name}
@ -168,9 +166,11 @@ function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) {
{showConfirmBuyModal && ( {showConfirmBuyModal && (
<ConfirmBuyModal <ConfirmBuyModal
item={item} item={currentItem}
onConfirm={handleConfirmBuy} onConfirm={handleConfirmBuy}
onCancel={handleCancelBuy} onCancel={handleCancelBuy}
allItems={allItems}
onNavigate={handleNavigate}
/> />
)} )}
</> </>
@ -187,9 +187,11 @@ export default memo(GroceryListItem, (prevProps, nextProps) => {
prevProps.item.item_image === nextProps.item.item_image && prevProps.item.item_image === nextProps.item.item_image &&
prevProps.item.bought === nextProps.item.bought && prevProps.item.bought === nextProps.item.bought &&
prevProps.item.last_added_on === nextProps.item.last_added_on && prevProps.item.last_added_on === nextProps.item.last_added_on &&
prevProps.item.zone === nextProps.item.zone &&
prevProps.item.added_by_users?.join(',') === nextProps.item.added_by_users?.join(',') && prevProps.item.added_by_users?.join(',') === nextProps.item.added_by_users?.join(',') &&
prevProps.onClick === nextProps.onClick && prevProps.onClick === nextProps.onClick &&
prevProps.onImageAdded === nextProps.onImageAdded && prevProps.onImageAdded === nextProps.onImageAdded &&
prevProps.onLongPress === nextProps.onLongPress prevProps.onLongPress === nextProps.onLongPress &&
prevProps.allItems?.length === nextProps.allItems?.length
); );
}); });

View File

@ -1,10 +1,21 @@
import { useState } from "react"; import { useState } from "react";
import "../../styles/ConfirmBuyModal.css"; import "../../styles/ConfirmBuyModal.css";
export default function ConfirmBuyModal({ item, onConfirm, onCancel }) { export default function ConfirmBuyModal({
item,
onConfirm,
onCancel,
allItems = [],
onNavigate
}) {
const [quantity, setQuantity] = useState(item.quantity); const [quantity, setQuantity] = useState(item.quantity);
const maxQuantity = item.quantity; const maxQuantity = item.quantity;
// Find current index and check for prev/next
const currentIndex = allItems.findIndex(i => i.id === item.id);
const hasPrev = currentIndex > 0;
const hasNext = currentIndex < allItems.length - 1;
const handleIncrement = () => { const handleIncrement = () => {
if (quantity < maxQuantity) { if (quantity < maxQuantity) {
setQuantity(prev => prev + 1); setQuantity(prev => prev + 1);
@ -21,14 +32,61 @@ export default function ConfirmBuyModal({ item, onConfirm, onCancel }) {
onConfirm(quantity); onConfirm(quantity);
}; };
const handlePrev = () => {
if (hasPrev && onNavigate) {
const prevItem = allItems[currentIndex - 1];
onNavigate(prevItem);
}
};
const handleNext = () => {
if (hasNext && onNavigate) {
const nextItem = allItems[currentIndex + 1];
onNavigate(nextItem);
}
};
const imageUrl = item.item_image && item.image_mime_type
? `data:${item.image_mime_type};base64,${item.item_image}`
: null;
return ( return (
<div className="confirm-buy-modal-overlay" onClick={onCancel}> <div className="confirm-buy-modal-overlay" onClick={onCancel}>
<div className="confirm-buy-modal" onClick={(e) => e.stopPropagation()}> <div className="confirm-buy-modal" onClick={(e) => e.stopPropagation()}>
<h2>Mark as Bought</h2> <div className="confirm-buy-header">
<p className="confirm-buy-item-name">"{item.item_name}"</p> {item.zone && <div className="confirm-buy-zone">{item.zone}</div>}
<h2 className="confirm-buy-item-name">{item.item_name}</h2>
</div>
<div className="confirm-buy-image-section">
<button
className="confirm-buy-nav-btn confirm-buy-nav-prev"
onClick={handlePrev}
style={{ visibility: hasPrev ? 'visible' : 'hidden' }}
disabled={!hasPrev}
>
</button>
<div className="confirm-buy-image-container">
{imageUrl ? (
<img src={imageUrl} alt={item.item_name} className="confirm-buy-image" />
) : (
<div className="confirm-buy-image-placeholder">📦</div>
)}
</div>
<button
className="confirm-buy-nav-btn confirm-buy-nav-next"
onClick={handleNext}
style={{ visibility: hasNext ? 'visible' : 'hidden' }}
disabled={!hasNext}
>
</button>
</div>
<div className="confirm-buy-quantity-section"> <div className="confirm-buy-quantity-section">
<p className="confirm-buy-label">Quantity to buy:</p>
<div className="confirm-buy-counter"> <div className="confirm-buy-counter">
<button <button
onClick={handleDecrement} onClick={handleDecrement}

View File

@ -1,14 +1,16 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import "../../styles/components/EditItemModal.css"; import "../../styles/components/EditItemModal.css";
import ClassificationSection from "../forms/ClassificationSection"; import ClassificationSection from "../forms/ClassificationSection";
import AddImageModal from "./AddImageModal";
export default function EditItemModal({ item, onSave, onCancel }) { export default function EditItemModal({ item, onSave, onCancel, onImageUpdate }) {
const [itemName, setItemName] = useState(item.item_name || ""); const [itemName, setItemName] = useState(item.item_name || "");
const [quantity, setQuantity] = useState(item.quantity || 1); const [quantity, setQuantity] = useState(item.quantity || 1);
const [itemType, setItemType] = useState(""); const [itemType, setItemType] = useState("");
const [itemGroup, setItemGroup] = useState(""); const [itemGroup, setItemGroup] = useState("");
const [zone, setZone] = useState(""); const [zone, setZone] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showImageModal, setShowImageModal] = useState(false);
// Load existing classification // Load existing classification
useEffect(() => { useEffect(() => {
@ -58,6 +60,18 @@ export default function EditItemModal({ item, onSave, onCancel }) {
} }
}; };
const handleImageUpload = async (imageFile) => {
if (onImageUpdate) {
try {
await onImageUpdate(item.id, itemName, quantity, imageFile);
setShowImageModal(false);
} catch (error) {
console.error("Failed to upload image:", error);
alert("Failed to upload image");
}
}
};
return ( return (
<div className="edit-modal-overlay" onClick={onCancel}> <div className="edit-modal-overlay" onClick={onCancel}>
<div className="edit-modal-content" onClick={(e) => e.stopPropagation()}> <div className="edit-modal-content" onClick={(e) => e.stopPropagation()}>
@ -97,6 +111,19 @@ export default function EditItemModal({ item, onSave, onCancel }) {
selectClass="edit-modal-select" selectClass="edit-modal-select"
/> />
<div className="edit-modal-divider" />
<div className="edit-modal-field">
<button
className="edit-modal-btn edit-modal-btn-image"
onClick={() => setShowImageModal(true)}
disabled={loading}
type="button"
>
{item.item_image ? "🖼️ Change Image" : "📷 Set Image"}
</button>
</div>
<div className="edit-modal-actions"> <div className="edit-modal-actions">
<button <button
className="edit-modal-btn edit-modal-btn-cancel" className="edit-modal-btn edit-modal-btn-cancel"
@ -114,6 +141,14 @@ export default function EditItemModal({ item, onSave, onCancel }) {
</button> </button>
</div> </div>
</div> </div>
{showImageModal && (
<AddImageModal
itemName={itemName}
onClose={() => setShowImageModal(false)}
onAddImage={handleImageUpload}
/>
)}
</div> </div>
); );
} }

View File

@ -377,8 +377,9 @@ export default function GroceryList() {
<GroceryListItem <GroceryListItem
key={item.id} key={item.id}
item={item} item={item}
onClick={(quantity) => allItems={sortedItems}
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(item.id, quantity) onClick={(id, quantity) =>
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity)
} }
onImageAdded={ onImageAdded={
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null [ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null
@ -398,8 +399,9 @@ export default function GroceryList() {
<GroceryListItem <GroceryListItem
key={item.id} key={item.id}
item={item} item={item}
onClick={(quantity) => allItems={sortedItems}
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(item.id, quantity) onClick={(id, quantity) =>
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity)
} }
onImageAdded={ onImageAdded={
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null [ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null
@ -420,6 +422,7 @@ export default function GroceryList() {
<GroceryListItem <GroceryListItem
key={item.id} key={item.id}
item={item} item={item}
allItems={recentlyBoughtItems}
onClick={null} onClick={null}
onImageAdded={ onImageAdded={
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null [ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null
@ -475,6 +478,7 @@ export default function GroceryList() {
item={editingItem} item={editingItem}
onSave={handleEditSave} onSave={handleEditSave}
onCancel={handleEditCancel} onCancel={handleEditCancel}
onImageUpdate={handleImageAdded}
/> />
)} )}
</div> </div>

View File

@ -14,7 +14,7 @@
.confirm-buy-modal { .confirm-buy-modal {
background: white; background: white;
padding: 2em; padding: 1em;
border-radius: 12px; border-radius: 12px;
max-width: 450px; max-width: 450px;
width: 90%; width: 90%;
@ -22,47 +22,107 @@
animation: slideUp 0.3s ease-out; animation: slideUp 0.3s ease-out;
} }
.confirm-buy-modal h2 { .confirm-buy-header {
margin: 0 0 0.5em 0;
font-size: 1.5em;
color: #333;
text-align: center; text-align: center;
margin-bottom: 0.5em;
}
.confirm-buy-zone {
font-size: 0.85em;
color: #666;
font-weight: 500;
margin-bottom: 0.2em;
text-transform: uppercase;
letter-spacing: 0.5px;
} }
.confirm-buy-item-name { .confirm-buy-item-name {
margin: 0 0 1.5em 0; margin: 0;
font-size: 1.1em; font-size: 1.2em;
color: #007bff; color: #007bff;
font-weight: 600; font-weight: 600;
text-align: center; }
.confirm-buy-image-section {
display: flex;
align-items: center;
justify-content: center;
gap: 0.6em;
margin: 0.8em 0;
}
.confirm-buy-nav-btn {
width: 35px;
height: 35px;
border: 2px solid #007bff;
border-radius: 50%;
background: white;
color: #007bff;
font-size: 1.8em;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
padding: 0;
flex-shrink: 0;
}
.confirm-buy-nav-btn:hover:not(:disabled) {
background: #007bff;
color: white;
}
.confirm-buy-nav-btn:disabled {
border-color: #ccc;
color: #ccc;
cursor: not-allowed;
}
.confirm-buy-image-container {
width: 280px;
height: 280px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #ddd;
border-radius: 8px;
overflow: hidden;
background: #f8f9fa;
}
.confirm-buy-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.confirm-buy-image-placeholder {
font-size: 4em;
color: #ccc;
} }
.confirm-buy-quantity-section { .confirm-buy-quantity-section {
margin: 2em 0; margin: 0.8em 0;
}
.confirm-buy-label {
margin: 0 0 1em 0;
font-size: 1em;
color: #555;
text-align: center;
} }
.confirm-buy-counter { .confirm-buy-counter {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 1em; gap: 0.8em;
} }
.confirm-buy-counter-btn { .confirm-buy-counter-btn {
width: 50px; width: 45px;
height: 50px; height: 45px;
border: 2px solid #007bff; border: 2px solid #007bff;
border-radius: 8px; border-radius: 8px;
background: white; background: white;
color: #007bff; color: #007bff;
font-size: 1.8em; font-size: 1.6em;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
@ -85,12 +145,12 @@
} }
.confirm-buy-counter-display { .confirm-buy-counter-display {
width: 80px; width: 70px;
height: 50px; height: 45px;
border: 2px solid #ddd; border: 2px solid #ddd;
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
font-size: 1.5em; font-size: 1.4em;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
background: #f8f9fa; background: #f8f9fa;
@ -103,20 +163,21 @@
.confirm-buy-actions { .confirm-buy-actions {
display: flex; display: flex;
gap: 1em; gap: 0.6em;
margin-top: 2em; margin-top: 1em;
} }
.confirm-buy-cancel, .confirm-buy-cancel,
.confirm-buy-confirm { .confirm-buy-confirm {
flex: 1; flex: 1;
padding: 0.9em; padding: 0.75em 0.5em;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
font-size: 1em; font-size: 0.95em;
font-weight: 500; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
white-space: nowrap;
} }
.confirm-buy-cancel { .confirm-buy-cancel {
@ -156,3 +217,88 @@
opacity: 1; opacity: 1;
} }
} }
/* Mobile optimizations */
@media (max-width: 480px) {
.confirm-buy-modal {
padding: 0.8em;
}
.confirm-buy-header {
margin-bottom: 0.4em;
}
.confirm-buy-zone {
font-size: 0.8em;
}
.confirm-buy-item-name {
font-size: 1.1em;
}
.confirm-buy-image-section {
gap: 0.5em;
margin: 0.6em 0;
}
.confirm-buy-actions {
gap: 0.5em;
margin-top: 0.8em;
}
.confirm-buy-cancel,
.confirm-buy-confirm {
padding: 0.7em 0.4em;
font-size: 0.9em;
}
.confirm-buy-image-container {
width: 220px;
height: 220px;
}
.confirm-buy-nav-btn {
width: 30px;
height: 30px;
font-size: 1.6em;
}
.confirm-buy-counter-btn {
width: 40px;
height: 40px;
font-size: 1.4em;
}
.confirm-buy-counter-display {
width: 60px;
height: 40px;
font-size: 1.2em;
}
.confirm-buy-quantity-section {
margin: 0.6em 0;
}
}
@media (max-width: 360px) {
.confirm-buy-modal {
padding: 0.7em;
}
.confirm-buy-cancel,
.confirm-buy-confirm {
padding: 0.65em 0.3em;
font-size: 0.85em;
}
.confirm-buy-image-container {
width: 180px;
height: 180px;
}
.confirm-buy-nav-btn {
width: 28px;
height: 28px;
font-size: 1.4em;
}
}

View File

@ -110,3 +110,26 @@
.edit-modal-btn-save:hover:not(:disabled) { .edit-modal-btn-save:hover:not(:disabled) {
background: #0056b3; background: #0056b3;
} }
.edit-modal-btn-image {
width: 100%;
padding: 0.7em;
font-size: 1em;
border: 2px solid #28a745;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
background: white;
color: #28a745;
}
.edit-modal-btn-image:hover:not(:disabled) {
background: #28a745;
color: white;
}
.edit-modal-btn-image:disabled {
opacity: 0.6;
cursor: not-allowed;
}