cleaned up grocerylist page, created reference document for cleaning up files, and consolidated all readme docs within docs folder

This commit is contained in:
Nico 2026-01-22 01:06:48 -08:00
parent bc7e212eea
commit 0c16d22c1e
8 changed files with 521 additions and 16 deletions

473
docs/code-cleanup-guide.md Normal file
View File

@ -0,0 +1,473 @@
# Code Cleanup Guide
This guide documents the cleanup patterns and best practices applied to the codebase, starting with `GroceryList.jsx`. Use this as a reference for maintaining consistent, clean, and readable code across all files.
## Table of Contents
1. [Spacing & Organization](#spacing--organization)
2. [Comment Formatting](#comment-formatting)
3. [Code Simplification](#code-simplification)
4. [React Performance Patterns](#react-performance-patterns)
5. [Cleanup Checklist](#cleanup-checklist)
---
## Spacing & Organization
### Two-Line Separation
Use **2 blank lines** to separate logical groups and functions.
**Before:**
```javascript
const handleAdd = async (itemName, quantity) => {
// function body
};
const handleBought = async (id) => {
// function body
};
```
**After:**
```javascript
const handleAdd = async (itemName, quantity) => {
// function body
};
const handleBought = async (id) => {
// function body
};
```
### Logical Grouping
Organize code into clear sections:
- State declarations
- Data loading functions
- Computed values (useMemo)
- Event handlers grouped by functionality
- Helper functions
- Render logic
---
## Comment Formatting
### Section Headers
Use the `=== Section Name ===` format for major sections, followed by 2 blank lines before the next code block.
**Pattern:**
```javascript
// === State ===
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
// === Data Loading ===
const loadItems = async () => {
// implementation
};
// === Event Handlers ===
const handleClick = () => {
// implementation
};
```
### Common Section Names
- `=== State ===`
- `=== Data Loading ===`
- `=== Computed Values ===` or `=== Sorted Items Computation ===`
- `=== Event Handlers ===` or specific groups like `=== Item Addition Handlers ===`
- `=== Helper Functions ===`
- `=== Render ===`
---
## Code Simplification
### 1. Optional Chaining
Replace `&&` null checks with optional chaining when accessing nested properties.
**Before:**
```javascript
if (existingItem && existingItem.bought === false) {
// do something
}
```
**After:**
```javascript
if (existingItem?.bought === false) {
// do something
}
```
**When to use:**
- Accessing properties on potentially undefined/null objects
- Checking nested properties: `user?.profile?.name`
- Method calls: `item?.toString?.()`
**When NOT to use:**
- When you need to check if the object exists first (use explicit check)
- For boolean coercion: `if (item)` is clearer than `if (item?.)`
---
### 2. Ternary Operators
Use ternary operators for simple conditional assignments and returns.
**Before:**
```javascript
let result;
if (condition) {
result = "yes";
} else {
result = "no";
}
```
**After:**
```javascript
const result = condition ? "yes" : "no";
```
**When to use:**
- Simple conditional assignments
- Inline JSX conditionals
- Return statements with simple conditions
**When NOT to use:**
- Complex multi-line logic (use if/else for readability)
- Nested ternaries (hard to read)
---
### 3. Early Returns
Use early returns to reduce nesting.
**Before:**
```javascript
const handleSuggest = async (text) => {
if (text.trim()) {
// long implementation
} else {
setSuggestions([]);
setButtonText("Add Item");
}
};
```
**After:**
```javascript
const handleSuggest = async (text) => {
if (!text.trim()) {
setSuggestions([]);
setButtonText("Add Item");
return;
}
// main implementation without nesting
};
```
---
### 4. Destructuring
Use destructuring for cleaner variable access.
**Before:**
```javascript
const username = user.username;
const email = user.email;
const role = user.role;
```
**After:**
```javascript
const { username, email, role } = user;
```
---
### 5. Array Methods Over Loops
Prefer array methods (`.map()`, `.filter()`, `.find()`) over traditional loops.
**Before:**
```javascript
const activeItems = [];
for (let i = 0; i < items.length; i++) {
if (!items[i].bought) {
activeItems.push(items[i]);
}
}
```
**After:**
```javascript
const activeItems = items.filter(item => !item.bought);
```
---
## React Performance Patterns
### 1. useCallback for Event Handlers
Wrap event handlers in `useCallback` to prevent unnecessary re-renders of child components.
```javascript
const handleBought = useCallback(async (id, quantity) => {
await markBought(id);
setItems(prevItems => prevItems.filter(item => item.id !== id));
loadRecentlyBought();
}, []); // Add dependencies if needed
```
**When to use:**
- Handler functions passed as props to memoized child components
- Functions used as dependencies in other hooks
- Functions in frequently re-rendering components
---
### 2. useMemo for Expensive Computations
Use `useMemo` for computationally expensive operations or large transformations.
```javascript
const sortedItems = useMemo(() => {
const sorted = [...items];
if (sortMode === "az") {
sorted.sort((a, b) => a.item_name.localeCompare(b.item_name));
}
return sorted;
}, [items, sortMode]);
```
**When to use:**
- Sorting/filtering large arrays
- Complex calculations
- Derived state that's expensive to compute
---
### 3. React.memo for Components
Wrap components with `React.memo` and provide custom comparison functions to prevent unnecessary re-renders.
```javascript
const GroceryListItem = React.memo(
({ item, onClick, onLongPress }) => {
// component implementation
},
(prevProps, nextProps) => {
return (
prevProps.id === nextProps.id &&
prevProps.item_name === nextProps.item_name &&
prevProps.quantity === nextProps.quantity &&
prevProps.item_image === nextProps.item_image
);
}
);
```
**When to use:**
- List item components that render frequently
- Components with stable props
- Pure components (output depends only on props)
---
### 4. In-Place State Updates
Update specific items in state instead of reloading entire datasets.
**Before:**
```javascript
const handleUpdate = async (id, newData) => {
await updateItem(id, newData);
loadItems(); // Reloads entire list from server
};
```
**After:**
```javascript
const handleUpdate = useCallback(async (id, newData) => {
const response = await updateItem(id, newData);
setItems(prevItems =>
prevItems.map(item =>
item.id === id ? { ...item, ...response.data } : item
)
);
}, []);
```
**Benefits:**
- Faster updates (no server round-trip for entire list)
- Preserves scroll position
- Better user experience (no full re-render)
---
## Cleanup Checklist
Use this checklist when cleaning up a file:
### Structure & Organization
- [ ] Group related state variables together
- [ ] Use 2-line spacing between logical sections
- [ ] Add section comments using `=== Format ===`
- [ ] Order sections logically (state → data loading → computed → handlers → helpers → render)
### Code Simplification
- [ ] Replace `&&` null checks with optional chaining where appropriate
- [ ] Convert simple if/else to ternary operators
- [ ] Use early returns to reduce nesting
- [ ] Apply destructuring for cleaner variable access
- [ ] Use array methods instead of loops
### React Performance
- [ ] Wrap stable event handlers in `useCallback`
- [ ] Use `useMemo` for expensive computations
- [ ] Consider `React.memo` for list items or frequently re-rendering components
- [ ] Update state in-place instead of reloading from server
### Consistency
- [ ] Check naming conventions (camelCase for functions/variables)
- [ ] Ensure consistent spacing and indentation
- [ ] Remove unused imports and variables
- [ ] Remove console.logs (except intentional debugging aids)
### Testing After Cleanup
- [ ] Verify no functionality broke
- [ ] Check that performance improved (using React DevTools Profiler)
- [ ] Test all interactive features
- [ ] Verify mobile/responsive behavior still works
---
## Example: Before & After
### Before Cleanup
```javascript
import { useState, useEffect } from "react";
export default function MyComponent() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const loadItems = async () => {
setLoading(true);
const res = await getItems();
setItems(res.data);
setLoading(false);
};
useEffect(() => {
loadItems();
}, []);
const handleUpdate = async (id, data) => {
await updateItem(id, data);
loadItems();
};
const handleDelete = async (id) => {
await deleteItem(id);
loadItems();
};
if (loading) return <p>Loading...</p>;
return (
<div>
{items.map(item => (
<Item key={item.id} item={item} onUpdate={handleUpdate} onDelete={handleDelete} />
))}
</div>
);
}
```
### After Cleanup
```javascript
import { useCallback, useEffect, useMemo, useState } from "react";
export default function MyComponent() {
// === State ===
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
// === Data Loading ===
const loadItems = async () => {
setLoading(true);
const res = await getItems();
setItems(res.data);
setLoading(false);
};
useEffect(() => {
loadItems();
}, []);
// === Event Handlers ===
const handleUpdate = useCallback(async (id, data) => {
const response = await updateItem(id, data);
setItems(prevItems =>
prevItems.map(item =>
item.id === id ? { ...item, ...response.data } : item
)
);
}, []);
const handleDelete = useCallback(async (id) => {
await deleteItem(id);
setItems(prevItems => prevItems.filter(item => item.id !== id));
}, []);
// === Render ===
if (loading) return <p>Loading...</p>;
return (
<div>
{items.map(item => (
<Item
key={item.id}
item={item}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>
))}
</div>
);
}
```
**Key improvements:**
1. Added section comments for clarity
2. Proper 2-line spacing between sections
3. Wrapped handlers in `useCallback` for performance
4. In-place state updates instead of reloading entire list
5. Better import organization
---
## Additional Resources
- [React Performance Optimization](https://react.dev/learn/render-and-commit)
- [useCallback Hook](https://react.dev/reference/react/useCallback)
- [useMemo Hook](https://react.dev/reference/react/useMemo)
- [React.memo](https://react.dev/reference/react/memo)
---
## Notes
- **Don't over-optimize**: Not every component needs `useCallback`/`useMemo`. Apply these patterns when you have measurable performance issues or when working with large lists.
- **Readability first**: If a simplification makes code harder to understand, skip it. Code should be optimized for human reading first.
- **Test thoroughly**: Always test after cleanup to ensure no functionality broke.
- **Incremental cleanup**: Don't try to clean up everything at once. Focus on one file at a time.
---
**Last Updated**: Based on GroceryList.jsx cleanup (January 2026)

View File

@ -1,5 +1,15 @@
import { useCallback, useContext, useEffect, useMemo, 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";
import AddItemForm from "../components/forms/AddItemForm"; import AddItemForm from "../components/forms/AddItemForm";
@ -13,9 +23,11 @@ import { AuthContext } from "../context/AuthContext";
import "../styles/pages/GroceryList.css"; import "../styles/pages/GroceryList.css";
import { findSimilarItems } from "../utils/stringSimilarity"; import { findSimilarItems } from "../utils/stringSimilarity";
export default function GroceryList() { export default function GroceryList() {
const { role } = useContext(AuthContext); const { role } = useContext(AuthContext);
// === State === //
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);
@ -31,6 +43,8 @@ export default function GroceryList() {
const [showEditModal, setShowEditModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false);
const [editingItem, setEditingItem] = useState(null); const [editingItem, setEditingItem] = useState(null);
// === Data Loading ===
const loadItems = async () => { const loadItems = async () => {
setLoading(true); setLoading(true);
const res = await getList(); const res = await getList();
@ -39,6 +53,7 @@ export default function GroceryList() {
setLoading(false); setLoading(false);
}; };
const loadRecentlyBought = async () => { const loadRecentlyBought = async () => {
try { try {
const res = await getRecentlyBought(); const res = await getRecentlyBought();
@ -49,11 +64,14 @@ export default function GroceryList() {
} }
}; };
useEffect(() => { useEffect(() => {
loadItems(); loadItems();
loadRecentlyBought(); loadRecentlyBought();
}, []); }, []);
// === Sorted Items Computation ===
const sortedItems = useMemo(() => { const sortedItems = useMemo(() => {
const sorted = [...items]; const sorted = [...items];
@ -95,6 +113,8 @@ export default function GroceryList() {
return sorted; return sorted;
}, [items, sortMode]); }, [items, sortMode]);
// === Suggestion Handler ===
const handleSuggest = async (text) => { const handleSuggest = async (text) => {
if (!text.trim()) { if (!text.trim()) {
setSuggestions([]); setSuggestions([]);
@ -106,26 +126,21 @@ export default function GroceryList() {
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);
if (exactMatch) { setButtonText(exactMatch ? "Add" : "Create + Add");
setButtonText("Add");
} else {
setButtonText("Create + Add");
}
try { try {
let suggestions = await getSuggestions(text); const suggestions = await getSuggestions(text);
suggestions = suggestions.data.map(s => s.item_name); setSuggestions(suggestions.data.map(s => s.item_name));
setSuggestions(suggestions);
} catch { } catch {
setSuggestions([]); setSuggestions([]);
} }
}; };
// === Item Addition Handlers ===
const handleAdd = useCallback(async (itemName, quantity) => { const handleAdd = useCallback(async (itemName, quantity) => {
if (!itemName.trim()) return; if (!itemName.trim()) return;
const lowerItemName = itemName.toLowerCase().trim();
let existingItem = null; let existingItem = null;
try { try {
const response = await getItemByName(itemName); const response = await getItemByName(itemName);
@ -153,6 +168,7 @@ export default function GroceryList() {
}); });
}, [recentlyBoughtItems]); }, [recentlyBoughtItems]);
const processItemAddition = useCallback(async (itemName, quantity) => { const processItemAddition = useCallback(async (itemName, quantity) => {
let existingItem = null; let existingItem = null;
try { try {
@ -162,7 +178,7 @@ export default function GroceryList() {
existingItem = null; existingItem = null;
} }
if (existingItem && existingItem.bought === false) { if (existingItem?.bought === false) {
const currentQuantity = existingItem.quantity; const currentQuantity = existingItem.quantity;
const newQuantity = currentQuantity + quantity; const newQuantity = currentQuantity + quantity;
const yes = window.confirm( const yes = window.confirm(
@ -196,11 +212,14 @@ export default function GroceryList() {
} }
}, []); }, []);
// === Similar Item Modal Handlers ===
const handleSimilarCancel = useCallback(() => { const handleSimilarCancel = useCallback(() => {
setShowSimilarModal(false); setShowSimilarModal(false);
setSimilarItemSuggestion(null); setSimilarItemSuggestion(null);
}, []); }, []);
const handleSimilarNo = useCallback(async () => { const handleSimilarNo = useCallback(async () => {
if (!similarItemSuggestion) return; if (!similarItemSuggestion) return;
setShowSimilarModal(false); setShowSimilarModal(false);
@ -208,6 +227,7 @@ export default function GroceryList() {
setSimilarItemSuggestion(null); setSimilarItemSuggestion(null);
}, [similarItemSuggestion, processItemAddition]); }, [similarItemSuggestion, processItemAddition]);
const handleSimilarYes = useCallback(async () => { const handleSimilarYes = useCallback(async () => {
if (!similarItemSuggestion) return; if (!similarItemSuggestion) return;
setShowSimilarModal(false); setShowSimilarModal(false);
@ -215,6 +235,8 @@ export default function GroceryList() {
setSimilarItemSuggestion(null); setSimilarItemSuggestion(null);
}, [similarItemSuggestion, processItemAddition]); }, [similarItemSuggestion, processItemAddition]);
// === Add Details Modal Handlers ===
const handleAddDetailsConfirm = useCallback(async (imageFile, classification) => { const handleAddDetailsConfirm = useCallback(async (imageFile, classification) => {
if (!pendingItem) return; if (!pendingItem) return;
@ -243,6 +265,7 @@ export default function GroceryList() {
} }
}, [pendingItem]); }, [pendingItem]);
const handleAddDetailsSkip = useCallback(async () => { const handleAddDetailsSkip = useCallback(async () => {
if (!pendingItem) return; if (!pendingItem) return;
@ -261,7 +284,8 @@ export default function GroceryList() {
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 handleAddDetailsCancel = useCallback(() => { const handleAddDetailsCancel = useCallback(() => {
setShowAddDetailsModal(false); setShowAddDetailsModal(false);
@ -271,14 +295,14 @@ export default function GroceryList() {
}, []); }, []);
// === Item Action Handlers ===
const handleBought = useCallback(async (id, quantity) => { const handleBought = useCallback(async (id, quantity) => {
await markBought(id); await markBought(id);
setItems(prevItems => prevItems.filter(item => item.id !== id)); setItems(prevItems => prevItems.filter(item => item.id !== id));
loadRecentlyBought(); loadRecentlyBought();
}, []); }, []);
const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => { const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => {
try { try {
const response = await updateItemImage(id, itemName, quantity, imageFile); const response = await updateItemImage(id, itemName, quantity, imageFile);
@ -300,6 +324,7 @@ export default function GroceryList() {
} }
}, []); }, []);
const handleLongPress = useCallback(async (item) => { const handleLongPress = useCallback(async (item) => {
if (![ROLES.ADMIN, ROLES.EDITOR].includes(role)) return; if (![ROLES.ADMIN, ROLES.EDITOR].includes(role)) return;
@ -317,6 +342,8 @@ export default function GroceryList() {
} }
}, [role]); }, [role]);
// === Edit Modal Handlers ===
const handleEditSave = useCallback(async (id, itemName, quantity, classification) => { const handleEditSave = useCallback(async (id, itemName, quantity, classification) => {
try { try {
const response = await updateItemWithClassification(id, itemName, quantity, classification); const response = await updateItemWithClassification(id, itemName, quantity, classification);
@ -337,15 +364,18 @@ export default function GroceryList() {
); );
} 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;
} }
}, []); }, []);
const handleEditCancel = useCallback(() => { const handleEditCancel = useCallback(() => {
setShowEditModal(false); setShowEditModal(false);
setEditingItem(null); setEditingItem(null);
}, []); }, []);
// === Helper Functions ===
const groupItemsByZone = (items) => { const groupItemsByZone = (items) => {
const groups = {}; const groups = {};
items.forEach(item => { items.forEach(item => {
@ -358,8 +388,10 @@ export default function GroceryList() {
return groups; return groups;
}; };
if (loading) return <p>Loading...</p>; if (loading) return <p>Loading...</p>;
return ( return (
<div className="glist-body"> <div className="glist-body">
<div className="glist-container"> <div className="glist-container">