# Recipe List Enhancement Plan ## Overview Enhance the All Recipes page (`/srv/docker-compose/basil/packages/web/src/pages/RecipeList.tsx`) with: - Pagination (12, 24, 48, All items per page) - Column controls (3, 6, 9 columns) - Size slider (7 levels: XS to XXL) - Search by title or tag ## Current State Analysis - **Backend**: Already supports `page`, `limit`, `search` params; returns `PaginatedResponse` - **Frontend**: Currently calls `recipesApi.getAll()` with NO parameters (loads only 20 recipes) - **Grid**: Uses `repeat(auto-fill, minmax(300px, 1fr))` with 200px image height - **Missing**: Tag search backend support, pagination UI, display controls ## Implementation Plan ### 1. Backend Enhancement - Tag Search **File**: `packages/api/src/routes/recipes.routes.ts` (around line 105) Add tag filtering support: ```typescript const { page = '1', limit = '20', search, cuisine, category, tag } = req.query; // In where clause: if (tag) { where.tags = { some: { tag: { name: { equals: tag as string, mode: 'insensitive' } } } }; } ``` ### 2. Frontend State Management **File**: `packages/web/src/pages/RecipeList.tsx` Add state variables: ```typescript // Pagination const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(24); const [totalRecipes, setTotalRecipes] = useState(0); // Display controls const [columnCount, setColumnCount] = useState<3 | 6 | 9>(6); const [cardSize, setCardSize] = useState(3); // 0-6 scale // Search const [searchInput, setSearchInput] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [searchType, setSearchType] = useState<'title' | 'tag'>('title'); const [availableTags, setAvailableTags] = useState([]); ``` **LocalStorage persistence** for: `itemsPerPage`, `columnCount`, `cardSize` **URL params** using `useSearchParams` for: `page`, `limit`, `search`, `type` ### 3. Size Presets Definition ```typescript const SIZE_PRESETS = { 0: { name: 'XS', minWidth: 150, imageHeight: 100 }, 1: { name: 'S', minWidth: 200, imageHeight: 133 }, 2: { name: 'M', minWidth: 250, imageHeight: 167 }, 3: { name: 'Default', minWidth: 300, imageHeight: 200 }, 4: { name: 'L', minWidth: 350, imageHeight: 233 }, 5: { name: 'XL', minWidth: 400, imageHeight: 267 }, 6: { name: 'XXL', minWidth: 500, imageHeight: 333 }, }; ``` ### 4. API Integration Update `loadRecipes` function to pass pagination and search params: ```typescript const params: any = { page: currentPage, limit: itemsPerPage === -1 ? 10000 : itemsPerPage, // -1 = "All" }; if (debouncedSearch) { if (searchType === 'title') { params.search = debouncedSearch; } else { params.tag = debouncedSearch; } } const response = await recipesApi.getAll(params); ``` ### 5. UI Layout Structure ``` ┌─────────────────────────────────────────────────────────┐ │ My Recipes │ ├─────────────────────────────────────────────────────────┤ │ Search: [___________] [Title/Tag Toggle] │ │ │ │ Display: [3] [6] [9] columns | Size: [====●==] │ │ │ │ Items: [12] [24] [48] [All] | Page: [◀ 1 of 5 ▶] │ └─────────────────────────────────────────────────────────┘ ``` Sticky toolbar with three sections: 1. **Search Section**: Input with title/tag toggle, datalist for tag autocomplete 2. **Display Controls**: Column buttons + size slider with labels 3. **Pagination Section**: Items per page buttons + page navigation ### 6. Dynamic Styling with CSS Variables **File**: `packages/web/src/styles/RecipeList.css` (NEW) ```css .recipe-grid { display: grid; grid-template-columns: repeat(var(--column-count), 1fr); gap: 1.5rem; } .recipe-card img { height: var(--recipe-image-height); object-fit: cover; } /* Responsive overrides */ @media (max-width: 768px) { .recipe-grid { grid-template-columns: repeat(1, 1fr) !important; } } ``` Apply via inline styles: ```typescript const gridStyle = { '--column-count': columnCount, '--recipe-image-height': `${SIZE_PRESETS[cardSize].imageHeight}px`, }; ``` ### 7. Search Debouncing Implement 400ms debounce to prevent API spam: ```typescript useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(searchInput); }, 400); return () => clearTimeout(timer); }, [searchInput]); ``` ### 8. Pagination Logic - Reset to page 1 when search/filters change - Handle "All" option with limit=10000 - Update URL params on state changes - Previous/Next buttons with disabled states - Display "Page X of Y" info ## Implementation Steps ### Phase 1: Backend (Tag Search) 1. Modify `packages/api/src/routes/recipes.routes.ts` - Add `tag` parameter extraction - Add tag filtering to Prisma where clause 2. Test: `GET /api/recipes?tag=italian` ### Phase 2: Frontend Foundation 1. Create `packages/web/src/styles/RecipeList.css` 2. Update `RecipeList.tsx`: - Add all state variables - Add localStorage load/save - Add URL params sync 3. Update `packages/web/src/services/api.ts`: - Add `tag?: string` to getAll params type ### Phase 3: Search UI 1. Search input with debouncing 2. Title/Tag toggle buttons 3. Fetch and populate available tags 4. Datalist autocomplete for tags 5. Wire to API call ### Phase 4: Display Controls 1. Column count buttons (3, 6, 9) 2. Size slider (0-6 range) with visual labels 3. CSS variables for dynamic styling 4. Wire to state with localStorage persistence ### Phase 5: Pagination UI 1. Items per page selector (12, 24, 48, All) 2. Page navigation (Previous/Next buttons) 3. Page info display 4. Wire to API pagination 5. Reset page on filter changes ### Phase 6: Integration & Polish 1. Combine all controls in sticky toolbar 2. Apply dynamic styles to grid 3. Responsive CSS media queries 4. Test all interactions 5. Fix UI/UX issues ### Phase 7: Testing 1. Unit tests for RecipeList component 2. E2E tests for main flows 3. Manual testing on different screen sizes ## Critical Files **Must Create:** - `packages/web/src/styles/RecipeList.css` **Must Modify:** - `packages/web/src/pages/RecipeList.tsx` (main implementation) - `packages/api/src/routes/recipes.routes.ts` (tag search) - `packages/web/src/services/api.ts` (TypeScript types) **Reference for Patterns:** - `packages/web/src/pages/Cookbooks.tsx` (UI controls, state management) - `packages/web/src/contexts/AuthContext.tsx` (localStorage patterns) ## Verification Steps 1. **Pagination**: Select "12 items per page", navigate to page 2, verify only 12 recipes shown 2. **Column Control**: Click "3 Columns", verify grid has 3 columns 3. **Size Slider**: Move slider to "XL", verify recipe cards and images increase in size 4. **Search by Title**: Type "pasta", verify filtered results (with debounce) 5. **Search by Tag**: Switch to "By Tag", type "italian", verify tagged recipes shown 6. **Persistence**: Refresh page, verify column count and size settings preserved 7. **URL Params**: Navigate to `/recipes?page=2&limit=24`, verify correct page loads 8. **Responsive**: Resize browser to mobile width, verify single column forced 9. **"All" Option**: Select "All", verify all recipes loaded 10. **Empty State**: Search for non-existent term, verify empty state displays ## Technical Decisions 1. **State Management**: React useState (no Redux needed) 2. **Backend Tag Search**: Extend API with `tag` parameter (preferred) 3. **URL Params**: Use for bookmarkable state 4. **Search Debounce**: 400ms delay 5. **"All" Pagination**: Send limit=10000 6. **CSS Organization**: Separate RecipeList.css file 7. **Size Levels**: 7 presets (XS to XXL) 8. **Column/Size**: Independent controls