196 lines
5.6 KiB
JavaScript
196 lines
5.6 KiB
JavaScript
import { memo, useRef, useState } from "react";
|
|
import AddImageModal from "../modals/AddImageModal";
|
|
import ConfirmBuyModal from "../modals/ConfirmBuyModal";
|
|
import ImageModal from "../modals/ImageModal";
|
|
|
|
function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) {
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [showAddImageModal, setShowAddImageModal] = useState(false);
|
|
const [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false);
|
|
|
|
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) {
|
|
setShowConfirmBuyModal(true);
|
|
}
|
|
};
|
|
|
|
const handleConfirmBuy = (quantity) => {
|
|
if (onClick) {
|
|
onClick(quantity);
|
|
}
|
|
setShowConfirmBuyModal(false);
|
|
};
|
|
|
|
const handleCancelBuy = () => {
|
|
setShowConfirmBuyModal(false);
|
|
};
|
|
|
|
const handleImageClick = (e) => {
|
|
e.stopPropagation(); // Prevent triggering the bought action
|
|
if (item.item_image) {
|
|
setShowModal(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 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"
|
|
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>
|
|
{item.added_by_users && item.added_by_users.length > 0 && (
|
|
<div className="glist-item-users">
|
|
{item.last_added_on && `${getTimeAgo(item.last_added_on)} -- `}
|
|
{item.added_by_users.join(" • ")}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</li>
|
|
|
|
{showModal && (
|
|
<ImageModal
|
|
imageUrl={imageUrl}
|
|
itemName={item.item_name}
|
|
onClose={() => setShowModal(false)}
|
|
/>
|
|
)}
|
|
|
|
{showAddImageModal && (
|
|
<AddImageModal
|
|
itemName={item.item_name}
|
|
onClose={() => setShowAddImageModal(false)}
|
|
onAddImage={handleAddImage}
|
|
/>
|
|
)}
|
|
|
|
{showConfirmBuyModal && (
|
|
<ConfirmBuyModal
|
|
item={item}
|
|
onConfirm={handleConfirmBuy}
|
|
onCancel={handleCancelBuy}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// 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.added_by_users?.join(',') === nextProps.item.added_by_users?.join(',') &&
|
|
prevProps.onClick === nextProps.onClick &&
|
|
prevProps.onImageAdded === nextProps.onImageAdded &&
|
|
prevProps.onLongPress === nextProps.onLongPress
|
|
);
|
|
});
|