improve on responsiveness with the use of react.memo
This commit is contained in:
parent
8d5b2d3ea3
commit
ce2574c454
@ -1,9 +1,9 @@
|
|||||||
import { 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";
|
import ImageModal from "../modals/ImageModal";
|
||||||
|
|
||||||
export default function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) {
|
function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) {
|
||||||
const [showModal, setShowModal] = useState(false);
|
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);
|
||||||
@ -176,3 +176,20 @@ export default function GroceryListItem({ item, onClick, onImageAdded, onLongPre
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { addItem, getClassification, getItemByName, getList, getRecentlyBought, getSuggestions, markBought, updateItemImage, updateItemWithClassification } from "../api/list";
|
import { addItem, getClassification, getItemByName, getList, getRecentlyBought, getSuggestions, markBought, updateItemImage, updateItemWithClassification } from "../api/list";
|
||||||
import FloatingActionButton from "../components/common/FloatingActionButton";
|
import FloatingActionButton from "../components/common/FloatingActionButton";
|
||||||
import SortDropdown from "../components/common/SortDropdown";
|
import SortDropdown from "../components/common/SortDropdown";
|
||||||
@ -18,7 +18,6 @@ export default function GroceryList() {
|
|||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
const [recentlyBoughtItems, setRecentlyBoughtItems] = useState([]);
|
const [recentlyBoughtItems, setRecentlyBoughtItems] = useState([]);
|
||||||
const [recentlyBoughtDisplayCount, setRecentlyBoughtDisplayCount] = useState(10);
|
const [recentlyBoughtDisplayCount, setRecentlyBoughtDisplayCount] = useState(10);
|
||||||
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(true);
|
const [showAddForm, setShowAddForm] = useState(true);
|
||||||
@ -54,8 +53,8 @@ export default function GroceryList() {
|
|||||||
loadRecentlyBought();
|
loadRecentlyBought();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const sortedItems = useMemo(() => {
|
||||||
let sorted = [...items];
|
const sorted = [...items];
|
||||||
|
|
||||||
if (sortMode === "az") sorted.sort((a, b) => a.item_name.localeCompare(b.item_name));
|
if (sortMode === "az") sorted.sort((a, b) => a.item_name.localeCompare(b.item_name));
|
||||||
if (sortMode === "za") sorted.sort((a, b) => b.item_name.localeCompare(a.item_name));
|
if (sortMode === "za") sorted.sort((a, b) => b.item_name.localeCompare(a.item_name));
|
||||||
@ -63,29 +62,24 @@ export default function GroceryList() {
|
|||||||
if (sortMode === "qty-low") sorted.sort((a, b) => a.quantity - b.quantity);
|
if (sortMode === "qty-low") sorted.sort((a, b) => a.quantity - b.quantity);
|
||||||
if (sortMode === "zone") {
|
if (sortMode === "zone") {
|
||||||
sorted.sort((a, b) => {
|
sorted.sort((a, b) => {
|
||||||
// Items without classification go to the end
|
|
||||||
if (!a.item_type && b.item_type) return 1;
|
if (!a.item_type && b.item_type) return 1;
|
||||||
if (a.item_type && !b.item_type) return -1;
|
if (a.item_type && !b.item_type) return -1;
|
||||||
if (!a.item_type && !b.item_type) return a.item_name.localeCompare(b.item_name);
|
if (!a.item_type && !b.item_type) return a.item_name.localeCompare(b.item_name);
|
||||||
|
|
||||||
// Sort by item_type
|
|
||||||
const typeCompare = (a.item_type || "").localeCompare(b.item_type || "");
|
const typeCompare = (a.item_type || "").localeCompare(b.item_type || "");
|
||||||
if (typeCompare !== 0) return typeCompare;
|
if (typeCompare !== 0) return typeCompare;
|
||||||
|
|
||||||
// Then by item_group
|
|
||||||
const groupCompare = (a.item_group || "").localeCompare(b.item_group || "");
|
const groupCompare = (a.item_group || "").localeCompare(b.item_group || "");
|
||||||
if (groupCompare !== 0) return groupCompare;
|
if (groupCompare !== 0) return groupCompare;
|
||||||
|
|
||||||
// Then by zone
|
|
||||||
const zoneCompare = (a.zone || "").localeCompare(b.zone || "");
|
const zoneCompare = (a.zone || "").localeCompare(b.zone || "");
|
||||||
if (zoneCompare !== 0) return zoneCompare;
|
if (zoneCompare !== 0) return zoneCompare;
|
||||||
|
|
||||||
// Finally by name
|
|
||||||
return a.item_name.localeCompare(b.item_name);
|
return a.item_name.localeCompare(b.item_name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setSortedItems(sorted);
|
return sorted;
|
||||||
}, [items, sortMode]);
|
}, [items, sortMode]);
|
||||||
|
|
||||||
const handleSuggest = async (text) => {
|
const handleSuggest = async (text) => {
|
||||||
@ -95,10 +89,7 @@ export default function GroceryList() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine both unbought and recently bought items for similarity checking
|
|
||||||
const allItems = [...items, ...recentlyBoughtItems];
|
const allItems = [...items, ...recentlyBoughtItems];
|
||||||
|
|
||||||
// Check if exact match exists (case-insensitive)
|
|
||||||
const lowerText = text.toLowerCase().trim();
|
const lowerText = text.toLowerCase().trim();
|
||||||
const exactMatch = allItems.find(item => item.item_name.toLowerCase() === lowerText);
|
const exactMatch = allItems.find(item => item.item_name.toLowerCase() === lowerText);
|
||||||
|
|
||||||
@ -117,12 +108,11 @@ export default function GroceryList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = async (itemName, quantity) => {
|
const handleAdd = useCallback(async (itemName, quantity) => {
|
||||||
if (!itemName.trim()) return;
|
if (!itemName.trim()) return;
|
||||||
|
|
||||||
const lowerItemName = itemName.toLowerCase().trim();
|
const lowerItemName = itemName.toLowerCase().trim();
|
||||||
|
|
||||||
// First check if exact item exists in database (case-insensitive)
|
|
||||||
let existingItem = null;
|
let existingItem = null;
|
||||||
try {
|
try {
|
||||||
const response = await getItemByName(itemName);
|
const response = await getItemByName(itemName);
|
||||||
@ -131,29 +121,26 @@ export default function GroceryList() {
|
|||||||
existingItem = null;
|
existingItem = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If exact item exists, skip similarity check and process directly
|
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
await processItemAddition(itemName, quantity);
|
await processItemAddition(itemName, quantity);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only check for similar items if exact item doesn't exist
|
setItems(prevItems => {
|
||||||
const allItems = [...items, ...recentlyBoughtItems];
|
const allItems = [...prevItems, ...recentlyBoughtItems];
|
||||||
const similar = findSimilarItems(itemName, allItems, 70);
|
const similar = findSimilarItems(itemName, allItems, 70);
|
||||||
if (similar.length > 0) {
|
if (similar.length > 0) {
|
||||||
// Show modal and wait for user decision
|
|
||||||
setSimilarItemSuggestion({ originalName: itemName, suggestedItem: similar[0], quantity });
|
setSimilarItemSuggestion({ originalName: itemName, suggestedItem: similar[0], quantity });
|
||||||
setShowSimilarModal(true);
|
setShowSimilarModal(true);
|
||||||
return;
|
return prevItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with normal flow for new items
|
processItemAddition(itemName, quantity);
|
||||||
await processItemAddition(itemName, quantity);
|
return prevItems;
|
||||||
};
|
});
|
||||||
|
}, [recentlyBoughtItems]);
|
||||||
|
|
||||||
const processItemAddition = async (itemName, quantity) => {
|
const processItemAddition = useCallback(async (itemName, quantity) => {
|
||||||
|
|
||||||
// Check if item exists in database (case-insensitive)
|
|
||||||
let existingItem = null;
|
let existingItem = null;
|
||||||
try {
|
try {
|
||||||
const response = await getItemByName(itemName);
|
const response = await getItemByName(itemName);
|
||||||
@ -163,7 +150,6 @@ export default function GroceryList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existingItem && existingItem.bought === false) {
|
if (existingItem && existingItem.bought === false) {
|
||||||
// Item exists and is unbought - update quantity
|
|
||||||
const currentQuantity = existingItem.quantity;
|
const currentQuantity = existingItem.quantity;
|
||||||
const newQuantity = currentQuantity + quantity;
|
const newQuantity = currentQuantity + quantity;
|
||||||
const yes = window.confirm(
|
const yes = window.confirm(
|
||||||
@ -171,117 +157,140 @@ export default function GroceryList() {
|
|||||||
);
|
);
|
||||||
if (!yes) return;
|
if (!yes) return;
|
||||||
|
|
||||||
await addItem(itemName, newQuantity, null);
|
const response = await addItem(itemName, newQuantity, null);
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setButtonText("Add Item");
|
setButtonText("Add Item");
|
||||||
loadItems();
|
|
||||||
|
if (response.data) {
|
||||||
|
setItems(prevItems =>
|
||||||
|
prevItems.map(item =>
|
||||||
|
item.id === existingItem.id ? { ...item, ...response.data } : item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (existingItem) {
|
} else if (existingItem) {
|
||||||
// Item exists in database (was previously bought) - just add quantity
|
const response = await addItem(itemName, quantity, null);
|
||||||
await addItem(itemName, quantity, null);
|
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setButtonText("Add Item");
|
setButtonText("Add Item");
|
||||||
loadItems();
|
|
||||||
|
if (response.data) {
|
||||||
|
setItems(prevItems => [...prevItems, response.data]);
|
||||||
|
setRecentlyBoughtItems(prevItems => prevItems.filter(item => item.id !== existingItem.id));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// NEW ITEM - show combined add details modal
|
|
||||||
setPendingItem({ itemName, quantity });
|
setPendingItem({ itemName, quantity });
|
||||||
setShowAddDetailsModal(true);
|
setShowAddDetailsModal(true);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleSimilarCancel = () => {
|
const handleSimilarCancel = useCallback(() => {
|
||||||
setShowSimilarModal(false);
|
setShowSimilarModal(false);
|
||||||
setSimilarItemSuggestion(null);
|
setSimilarItemSuggestion(null);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleSimilarNo = async () => {
|
const handleSimilarNo = useCallback(async () => {
|
||||||
if (!similarItemSuggestion) return;
|
if (!similarItemSuggestion) return;
|
||||||
setShowSimilarModal(false);
|
setShowSimilarModal(false);
|
||||||
// Create new item with original name
|
|
||||||
await processItemAddition(similarItemSuggestion.originalName, similarItemSuggestion.quantity);
|
await processItemAddition(similarItemSuggestion.originalName, similarItemSuggestion.quantity);
|
||||||
setSimilarItemSuggestion(null);
|
setSimilarItemSuggestion(null);
|
||||||
};
|
}, [similarItemSuggestion, processItemAddition]);
|
||||||
|
|
||||||
const handleSimilarYes = async () => {
|
const handleSimilarYes = useCallback(async () => {
|
||||||
if (!similarItemSuggestion) return;
|
if (!similarItemSuggestion) return;
|
||||||
setShowSimilarModal(false);
|
setShowSimilarModal(false);
|
||||||
// Use suggested item name
|
|
||||||
await processItemAddition(similarItemSuggestion.suggestedItem.item_name, similarItemSuggestion.quantity);
|
await processItemAddition(similarItemSuggestion.suggestedItem.item_name, similarItemSuggestion.quantity);
|
||||||
setSimilarItemSuggestion(null);
|
setSimilarItemSuggestion(null);
|
||||||
};
|
}, [similarItemSuggestion, processItemAddition]);
|
||||||
|
|
||||||
const handleAddDetailsConfirm = async (imageFile, classification) => {
|
const handleAddDetailsConfirm = useCallback(async (imageFile, classification) => {
|
||||||
if (!pendingItem) return;
|
if (!pendingItem) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Add item to grocery_list with image
|
const addResponse = await addItem(pendingItem.itemName, pendingItem.quantity, imageFile);
|
||||||
await addItem(pendingItem.itemName, pendingItem.quantity, imageFile);
|
let newItem = addResponse.data;
|
||||||
|
|
||||||
// If classification provided, add it
|
|
||||||
if (classification) {
|
if (classification) {
|
||||||
const itemResponse = await getItemByName(pendingItem.itemName);
|
const itemResponse = await getItemByName(pendingItem.itemName);
|
||||||
const itemId = itemResponse.data.id;
|
const itemId = itemResponse.data.id;
|
||||||
await updateItemWithClassification(itemId, undefined, undefined, classification);
|
const updateResponse = await updateItemWithClassification(itemId, undefined, undefined, classification);
|
||||||
|
newItem = { ...newItem, ...updateResponse.data };
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowAddDetailsModal(false);
|
setShowAddDetailsModal(false);
|
||||||
setPendingItem(null);
|
setPendingItem(null);
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setButtonText("Add Item");
|
setButtonText("Add Item");
|
||||||
loadItems();
|
|
||||||
|
if (newItem) {
|
||||||
|
setItems(prevItems => [...prevItems, newItem]);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to add item:", error);
|
console.error("Failed to add item:", error);
|
||||||
alert("Failed to add item. Please try again.");
|
alert("Failed to add item. Please try again.");
|
||||||
}
|
}
|
||||||
};
|
}, [pendingItem]);
|
||||||
|
|
||||||
const handleAddDetailsSkip = async () => {
|
const handleAddDetailsSkip = useCallback(async () => {
|
||||||
if (!pendingItem) return;
|
if (!pendingItem) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Add item without image or classification
|
const response = await addItem(pendingItem.itemName, pendingItem.quantity, null);
|
||||||
await addItem(pendingItem.itemName, pendingItem.quantity, null);
|
|
||||||
|
|
||||||
setShowAddDetailsModal(false);
|
setShowAddDetailsModal(false);
|
||||||
setPendingItem(null);
|
setPendingItem(null);
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setButtonText("Add Item");
|
setButtonText("Add Item");
|
||||||
loadItems();
|
|
||||||
|
if (response.data) {
|
||||||
|
setItems(prevItems => [...prevItems, response.data]);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to add item:", error);
|
console.error("Failed to add item:", error);
|
||||||
alert("Failed to add item. Please try again.");
|
alert("Failed to add item. Please try again.");
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleAddDetailsCancel = () => {
|
const handleAddDetailsCancel = useCallback(() => {
|
||||||
setShowAddDetailsModal(false);
|
setShowAddDetailsModal(false);
|
||||||
setPendingItem(null);
|
setPendingItem(null);
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setButtonText("Add Item");
|
setButtonText("Add Item");
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleBought = async (id, quantity) => {
|
const handleBought = useCallback(async (id, quantity) => {
|
||||||
await markBought(id);
|
await markBought(id);
|
||||||
loadItems();
|
|
||||||
loadRecentlyBought();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageAdded = async (id, itemName, quantity, imageFile) => {
|
setItems(prevItems => prevItems.filter(item => item.id !== id));
|
||||||
|
loadRecentlyBought();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => {
|
||||||
try {
|
try {
|
||||||
await updateItemImage(id, itemName, quantity, imageFile);
|
const response = await updateItemImage(id, itemName, quantity, imageFile);
|
||||||
loadItems(); // Reload to show new image
|
|
||||||
|
setItems(prevItems =>
|
||||||
|
prevItems.map(item =>
|
||||||
|
item.id === id ? { ...item, ...response.data } : item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setRecentlyBoughtItems(prevItems =>
|
||||||
|
prevItems.map(item =>
|
||||||
|
item.id === id ? { ...item, ...response.data } : item
|
||||||
|
)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to add image:", error);
|
console.error("Failed to add image:", error);
|
||||||
alert("Failed to add image. Please try again.");
|
alert("Failed to add image. Please try again.");
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleLongPress = async (item) => {
|
const handleLongPress = useCallback(async (item) => {
|
||||||
if (![ROLES.ADMIN, ROLES.EDITOR].includes(role)) return;
|
if (![ROLES.ADMIN, ROLES.EDITOR].includes(role)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch existing classification
|
|
||||||
const classificationResponse = await getClassification(item.id);
|
const classificationResponse = await getClassification(item.id);
|
||||||
setEditingItem({
|
setEditingItem({
|
||||||
...item,
|
...item,
|
||||||
@ -293,27 +302,37 @@ export default function GroceryList() {
|
|||||||
setEditingItem({ ...item, classification: null });
|
setEditingItem({ ...item, classification: null });
|
||||||
setShowEditModal(true);
|
setShowEditModal(true);
|
||||||
}
|
}
|
||||||
};
|
}, [role]);
|
||||||
|
|
||||||
const handleEditSave = async (id, itemName, quantity, classification) => {
|
const handleEditSave = useCallback(async (id, itemName, quantity, classification) => {
|
||||||
try {
|
try {
|
||||||
await updateItemWithClassification(id, itemName, quantity, classification);
|
const response = await updateItemWithClassification(id, itemName, quantity, classification);
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
setEditingItem(null);
|
setEditingItem(null);
|
||||||
loadItems();
|
|
||||||
loadRecentlyBought();
|
const updatedItem = response.data;
|
||||||
|
setItems(prevItems =>
|
||||||
|
prevItems.map(item =>
|
||||||
|
item.id === id ? { ...item, ...updatedItem } : item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setRecentlyBoughtItems(prevItems =>
|
||||||
|
prevItems.map(item =>
|
||||||
|
item.id === id ? { ...item, ...updatedItem } : item
|
||||||
|
)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update item:", error);
|
console.error("Failed to update item:", error);
|
||||||
throw error; // Re-throw to let modal handle it
|
throw error; // Re-throw to let modal handle it
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleEditCancel = () => {
|
const handleEditCancel = useCallback(() => {
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
setEditingItem(null);
|
setEditingItem(null);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// Group items by zone for classification view
|
|
||||||
const groupItemsByZone = (items) => {
|
const groupItemsByZone = (items) => {
|
||||||
const groups = {};
|
const groups = {};
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
@ -346,7 +365,6 @@ export default function GroceryList() {
|
|||||||
<SortDropdown value={sortMode} onChange={setSortMode} />
|
<SortDropdown value={sortMode} onChange={setSortMode} />
|
||||||
|
|
||||||
{sortMode === "zone" ? (
|
{sortMode === "zone" ? (
|
||||||
// Grouped view by zone
|
|
||||||
(() => {
|
(() => {
|
||||||
const grouped = groupItemsByZone(sortedItems);
|
const grouped = groupItemsByZone(sortedItems);
|
||||||
return Object.keys(grouped).map(zone => (
|
return Object.keys(grouped).map(zone => (
|
||||||
@ -375,7 +393,6 @@ export default function GroceryList() {
|
|||||||
));
|
));
|
||||||
})()
|
})()
|
||||||
) : (
|
) : (
|
||||||
// Regular flat list view
|
|
||||||
<ul className="glist-ul">
|
<ul className="glist-ul">
|
||||||
{sortedItems.map((item) => (
|
{sortedItems.map((item) => (
|
||||||
<GroceryListItem
|
<GroceryListItem
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user