All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m7s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 3s
Build & Deploy Costco Grocery List / deploy (push) Successful in 12s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
203 lines
5.9 KiB
JavaScript
203 lines
5.9 KiB
JavaScript
import { memo, useRef, useState } from "react";
|
|
import AddImageModal from "../modals/AddImageModal";
|
|
import ConfirmBuyModal from "../modals/ConfirmBuyModal";
|
|
|
|
function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems = [], compact = false }) {
|
|
const [showAddImageModal, setShowAddImageModal] = useState(false);
|
|
const [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false);
|
|
const [currentItem, setCurrentItem] = useState(item);
|
|
|
|
const longPressTimer = useRef(null);
|
|
const pressStartPos = useRef({ x: 0, y: 0 });
|
|
|
|
const handleTouchStart = (e) => {
|
|
const touch = e.touches[0];
|
|
pressStartPos.current = { x: touch.clientX, y: touch.clientY };
|
|
|
|
longPressTimer.current = setTimeout(() => {
|
|
if (onLongPress) {
|
|
onLongPress(item);
|
|
}
|
|
}, 500); // 500ms for long press
|
|
};
|
|
|
|
const handleTouchMove = (e) => {
|
|
// Cancel long press if finger moves too much
|
|
const touch = e.touches[0];
|
|
const moveDistance = Math.sqrt(
|
|
Math.pow(touch.clientX - pressStartPos.current.x, 2) +
|
|
Math.pow(touch.clientY - pressStartPos.current.y, 2)
|
|
);
|
|
|
|
if (moveDistance > 10) {
|
|
clearTimeout(longPressTimer.current);
|
|
}
|
|
};
|
|
|
|
const handleTouchEnd = () => {
|
|
clearTimeout(longPressTimer.current);
|
|
};
|
|
|
|
const handleMouseDown = () => {
|
|
longPressTimer.current = setTimeout(() => {
|
|
if (onLongPress) {
|
|
onLongPress(item);
|
|
}
|
|
}, 500);
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
clearTimeout(longPressTimer.current);
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
clearTimeout(longPressTimer.current);
|
|
};
|
|
|
|
const handleItemClick = () => {
|
|
if (onClick) {
|
|
setCurrentItem(item);
|
|
setShowConfirmBuyModal(true);
|
|
}
|
|
};
|
|
|
|
const handleConfirmBuy = (quantity) => {
|
|
if (onClick) {
|
|
onClick(currentItem.id, quantity);
|
|
}
|
|
setShowConfirmBuyModal(false);
|
|
};
|
|
|
|
const handleCancelBuy = () => {
|
|
setShowConfirmBuyModal(false);
|
|
};
|
|
|
|
const handleNavigate = (newItem) => {
|
|
setCurrentItem(newItem);
|
|
};
|
|
|
|
const handleImageClick = (e) => {
|
|
e.stopPropagation(); // Prevent triggering the bought action
|
|
if (item.item_image) {
|
|
// Open buy modal which now shows the image
|
|
setCurrentItem(item);
|
|
setShowConfirmBuyModal(true);
|
|
} else {
|
|
setShowAddImageModal(true);
|
|
}
|
|
};
|
|
|
|
const handleAddImage = async (imageFile) => {
|
|
if (onImageAdded) {
|
|
await onImageAdded(item.id, item.item_name, item.quantity, imageFile);
|
|
}
|
|
setShowAddImageModal(false);
|
|
};
|
|
|
|
const imageUrl = item.item_image && item.image_mime_type
|
|
? `data:${item.image_mime_type};base64,${item.item_image}`
|
|
: null;
|
|
const addedByUsers = Array.isArray(item.added_by_users)
|
|
? item.added_by_users.filter(
|
|
(name) => typeof name === "string" && name.trim().length > 0
|
|
)
|
|
: [];
|
|
|
|
const getTimeAgo = (dateString) => {
|
|
if (!dateString) return null;
|
|
|
|
const addedDate = new Date(dateString);
|
|
const now = new Date();
|
|
const diffMs = now - addedDate;
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffDays < 7) {
|
|
return `${diffDays}d ago`;
|
|
} else if (diffDays < 30) {
|
|
const weeks = Math.floor(diffDays / 7);
|
|
return `${weeks}w ago`;
|
|
} else {
|
|
const months = Math.floor(diffDays / 30);
|
|
return `${months}m ago`;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<li
|
|
className={`glist-li ${compact ? 'compact' : ''}`}
|
|
onClick={handleItemClick}
|
|
onTouchStart={handleTouchStart}
|
|
onTouchMove={handleTouchMove}
|
|
onTouchEnd={handleTouchEnd}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseUp={handleMouseUp}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
<div className="glist-item-layout">
|
|
<div
|
|
className={`glist-item-image ${item.item_image ? "has-image" : ""}`}
|
|
onClick={handleImageClick}
|
|
style={{ cursor: "pointer" }}
|
|
>
|
|
{item.item_image ? (
|
|
<img src={imageUrl} alt={item.item_name} />
|
|
) : (
|
|
<span>📦</span>
|
|
)}
|
|
<span className="glist-item-quantity">x{item.quantity}</span>
|
|
</div>
|
|
<div className="glist-item-content">
|
|
<div className="glist-item-header">
|
|
<span className="glist-item-name">{item.item_name}</span>
|
|
</div>
|
|
{addedByUsers.length > 0 && (
|
|
<div className="glist-item-users">
|
|
{item.last_added_on && `${getTimeAgo(item.last_added_on)} -- `}
|
|
{addedByUsers.join(" | ")}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</li>
|
|
|
|
{showAddImageModal && (
|
|
<AddImageModal
|
|
itemName={item.item_name}
|
|
onClose={() => setShowAddImageModal(false)}
|
|
onAddImage={handleAddImage}
|
|
/>
|
|
)}
|
|
|
|
{showConfirmBuyModal && (
|
|
<ConfirmBuyModal
|
|
item={currentItem}
|
|
onConfirm={handleConfirmBuy}
|
|
onCancel={handleCancelBuy}
|
|
allItems={allItems}
|
|
onNavigate={handleNavigate}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Memoize component to prevent re-renders when props haven't changed
|
|
export default memo(GroceryListItem, (prevProps, nextProps) => {
|
|
// Only re-render if the item data or handlers have changed
|
|
return (
|
|
prevProps.item.id === nextProps.item.id &&
|
|
prevProps.item.item_name === nextProps.item.item_name &&
|
|
prevProps.item.quantity === nextProps.item.quantity &&
|
|
prevProps.item.item_image === nextProps.item.item_image &&
|
|
prevProps.item.bought === nextProps.item.bought &&
|
|
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.onClick === nextProps.onClick &&
|
|
prevProps.onImageAdded === nextProps.onImageAdded &&
|
|
prevProps.onLongPress === nextProps.onLongPress &&
|
|
prevProps.allItems?.length === nextProps.allItems?.length
|
|
);
|
|
});
|