styling fix and readme files reorg

This commit is contained in:
Nico 2026-01-27 00:03:58 -08:00
parent 31eda793ab
commit 11f23eb643
34 changed files with 1442 additions and 95 deletions

View File

@ -7,40 +7,142 @@ This is a full-stack grocery list management app with **role-based access contro
- **Frontend**: React 19 + TypeScript + Vite (port 3000/5173)
- **Deployment**: Docker Compose with separate dev/prod configurations
## Mobile-First Design Principles
**CRITICAL**: All UI components MUST be designed for both mobile and desktop from the start.
**Responsive Design Requirements**:
- Use relative units (`rem`, `em`, `%`, `vh/vw`) over fixed pixels where possible
- Implement mobile breakpoints: `480px`, `768px`, `1024px`
- Test layouts at: 320px (small phone), 375px (phone), 768px (tablet), 1024px+ (desktop)
- Avoid horizontal scrolling on mobile devices
- Touch targets minimum 44x44px for mobile usability
- Use `max-width` with `margin: 0 auto` for content containers
- Stack elements vertically on mobile, use flexbox/grid for larger screens
- Hide/collapse navigation into hamburger menus on mobile
- Ensure modals/dropdowns work well on small screens
**Common Patterns**:
```css
/* Mobile-first approach */
.container {
padding: 1rem;
max-width: 100%;
}
@media (min-width: 768px) {
.container {
padding: 2rem;
max-width: 800px;
margin: 0 auto;
}
}
```
### Key Design Patterns
**Three-tier RBAC system** (`viewer`, `editor`, `admin`):
- `viewer`: Read-only access to grocery lists
- `editor`: Can add items and mark as bought
- `admin`: Full user management via admin panel
- Roles defined in [backend/models/user.model.js](backend/models/user.model.js) and mirrored in [frontend/src/constants/roles.js](frontend/src/constants/roles.js)
**Dual RBAC System** - Two separate role hierarchies:
**1. System Roles** (users.role column):
- `system_admin`: Access to Admin Panel for system-wide management (stores, users)
- `user`: Regular system user (default for new registrations)
- Defined in [backend/models/user.model.js](backend/models/user.model.js)
- Used for Admin Panel access control
**2. Household Roles** (household_members.role column):
- `admin`: Can manage household members, change roles, delete household
- `user`: Can add/edit items, mark as bought (standard member permissions)
- Defined per household membership
- Used for household-level permissions (item management, member management)
**Important**: Always distinguish between system role and household role:
- **System role**: From `AuthContext` or `req.user.role` - controls Admin Panel access
- **Household role**: From `activeHousehold.role` or `household_members.role` - controls household operations
**Middleware chain pattern** for protected routes:
```javascript
router.post("/add", auth, requireRole(ROLES.EDITOR, ROLES.ADMIN), controller.addItem);
// System-level protection
router.get("/stores", auth, requireRole("system_admin"), controller.getAllStores);
// Household-level checks done in controller
router.post("/lists/:householdId/items", auth, controller.addItem);
```
- `auth` middleware extracts JWT from `Authorization: Bearer <token>` header
- `requireRole` checks if user's role matches allowed roles
- See [backend/routes/list.routes.js](backend/routes/list.routes.js) for examples
- `requireRole` checks system role only
- Household role checks happen in controllers using `household.model.js` methods
**Frontend route protection**:
- `<PrivateRoute>`: Requires authentication, redirects to `/login` if no token
- `<RoleGuard allowed={[ROLES.ADMIN]}>`: Requires specific role(s), redirects to `/` if unauthorized
- `<RoleGuard allowed={[ROLES.SYSTEM_ADMIN]}>`: Requires system_admin role for Admin Panel
- Household permissions: Check `activeHousehold.role` in components (not route-level)
- Example in [frontend/src/App.jsx](frontend/src/App.jsx)
**Multi-Household Architecture**:
- Users can belong to multiple households
- Each household has its own grocery lists, stores, and item classifications
- `HouseholdContext` manages active household selection
- All list operations are scoped to the active household
## Database Schema
**PostgreSQL server runs externally** - not in Docker Compose. Connection configured in [backend/.env](backend/.env) via standard environment variables.
**Tables** (inferred from models, no formal migrations):
- **users**: `id`, `username`, `password` (bcrypt hashed), `name`, `role`
- **grocery_list**: `id`, `item_name`, `quantity`, `bought`, `added_by`
- **grocery_history**: Junction table tracking which users added which items
**Core Tables**:
**users** - System users
- `id` (PK), `username`, `password` (bcrypt), `name`, `display_name`
- `role`: `system_admin` | `user` (default: `viewer` - legacy)
- System-level authentication and authorization
**households** - Household entities
- `id` (PK), `name`, `invite_code`, `created_by`, `created_at`
- Each household is independent with own lists and members
**household_members** - Junction table (users ↔ households)
- `id` (PK), `household_id` (FK), `user_id` (FK), `role`, `joined_at`
- `role`: `admin` | `user` (household-level permissions)
- One user can belong to multiple households with different roles
**items** - Master item catalog
- `id` (PK), `name`, `default_image`, `default_image_mime_type`, `usage_count`
- Shared across all households, case-insensitive unique names
**stores** - Store definitions (system-wide)
- `id` (PK), `name`, `default_zones` (JSONB array)
- Managed by system_admin in Admin Panel
**household_stores** - Stores available to each household
- `id` (PK), `household_id` (FK), `store_id` (FK), `is_default`
- Links households to stores they use
**household_lists** - Grocery list items per household
- `id` (PK), `household_id` (FK), `store_id` (FK), `item_id` (FK)
- `quantity`, `bought`, `custom_image`, `custom_image_mime_type`
- `added_by`, `modified_on`
- Scoped to household + store combination
**household_list_history** - Tracks quantity contributions
- `id` (PK), `household_list_id` (FK), `quantity`, `added_by`, `added_on`
- Multi-contributor tracking (who added how much)
**household_item_classifications** - Item classifications per household/store
- `id` (PK), `household_id`, `store_id`, `item_id`
- `item_type`, `item_group`, `zone`, `confidence`, `source`
- Household-specific overrides of global classifications
**item_classification** - Global item classifications
- `id` (PK), `item_type`, `item_group`, `zone`, `confidence`, `source`
- System-wide defaults for item categorization
**Legacy Tables** (deprecated, may still exist):
- `grocery_list`, `grocery_history` - Old single-household implementation
**Important patterns**:
- No migration system - schema changes are manual SQL
- No formal migration system - schema changes are manual SQL
- Items use case-insensitive matching (`ILIKE`) to prevent duplicates
- JOINs with `ARRAY_AGG` for multi-contributor queries (see [backend/models/list.model.js](backend/models/list.model.js))
- JOINs with `ARRAY_AGG` for multi-contributor queries (see [backend/models/list.model.v2.js](backend/models/list.model.v2.js))
- All list operations require `household_id` parameter for scoping
- Image storage: `bytea` columns for images with separate MIME type columns
## Development Workflow
@ -137,11 +239,16 @@ See [.gitea/workflows/deploy.yml](.gitea/workflows/deploy.yml) for full workflow
## Authentication Flow
1. User logs in → backend returns `{token, role, username}` ([backend/controllers/auth.controller.js](backend/controllers/auth.controller.js))
1. User logs in → backend returns `{token, userId, role, username}` ([backend/controllers/auth.controller.js](backend/controllers/auth.controller.js))
- `role` is the **system role** (`system_admin` or `user`)
2. Frontend stores in `localStorage` and `AuthContext` ([frontend/src/context/AuthContext.jsx](frontend/src/context/AuthContext.jsx))
3. Axios interceptor auto-attaches `Authorization: Bearer <token>` header ([frontend/src/api/axios.js](frontend/src/api/axios.js))
4. Backend validates JWT on protected routes ([backend/middleware/auth.js](backend/middleware/auth.js))
5. On 401 "Invalid or expired token" response, frontend clears storage and redirects to login
3. `HouseholdContext` loads user's households and sets active household
- Active household includes `household.role` (the **household role**)
4. Axios interceptor auto-attaches `Authorization: Bearer <token>` header ([frontend/src/api/axios.js](frontend/src/api/axios.js))
5. Backend validates JWT on protected routes ([backend/middleware/auth.js](backend/middleware/auth.js))
- Sets `req.user = { id, role, username }` with **system role**
6. Controllers check household membership/role using [backend/models/household.model.js](backend/models/household.model.js)
7. On 401 "Invalid or expired token" response, frontend clears storage and redirects to login
## Critical Conventions
@ -167,16 +274,36 @@ See [.gitea/workflows/deploy.yml](.gitea/workflows/deploy.yml) for full workflow
## Common Tasks
**Add a new protected route**:
1. Backend: Add route with `auth` + `requireRole(...)` middleware
2. Frontend: Add route in [frontend/src/App.jsx](frontend/src/App.jsx) wrapped in `<PrivateRoute>` and/or `<RoleGuard>`
1. Backend: Add route with `auth` middleware (+ `requireRole(...)` if system role check needed)
2. Frontend: Add route in [frontend/src/App.jsx](frontend/src/App.jsx) wrapped in `<PrivateRoute>` (and `<RoleGuard>` for Admin Panel)
**Access user info in backend controller**:
```javascript
const { id, role } = req.user; // Set by auth middleware
const { id, role } = req.user; // Set by auth middleware (system role)
const userId = req.user.id;
```
**Check household permissions in backend controller**:
```javascript
const householdRole = await household.getUserRole(householdId, userId);
if (!householdRole) return res.status(403).json({ message: "Not a member of this household" });
if (householdRole !== 'admin') return res.status(403).json({ message: "Household admin required" });
```
**Check household permissions in frontend**:
```javascript
const { activeHousehold } = useContext(HouseholdContext);
const householdRole = activeHousehold?.role; // 'admin' or 'user'
// Allow all members except viewers (no viewer role in households)
const canManageItems = householdRole && householdRole !== 'viewer'; // Usually just check if role exists
// Admin-only actions
const canManageMembers = householdRole === 'admin';
```
**Query grocery items with contributors**:
Use the JOIN pattern in [backend/models/list.model.js](backend/models/list.model.js) - aggregates user names via `grocery_history` table.
Use the JOIN pattern in [backend/models/list.model.v2.js](backend/models/list.model.v2.js) - aggregates user names via `household_list_history` table.
## Testing

View File

@ -26,21 +26,20 @@ exports.getHouseholdStores = async (req, res) => {
exports.addStoreToHousehold = async (req, res) => {
try {
const { storeId, isDefault } = req.body;
// console.log("Adding store to household:", { householdId: req.params.householdId, storeId, isDefault });
if (!storeId) {
return res.status(400).json({ error: "Store ID is required" });
}
// Check if store exists
const store = await storeModel.getStoreById(storeId);
if (!store) {
return res.status(404).json({ error: "Store not found" });
}
if (!store) return res.status(404).json({ error: "Store not found" });
const foundStores = await storeModel.getHouseholdStores(req.params.householdId);
// if (foundStores.length == 0) isDefault = 'true';
await storeModel.addStoreToHousehold(
req.params.householdId,
storeId,
isDefault || false
foundStores.length == 0 ? true : isDefault || false
);
res.status(201).json({

View File

@ -94,7 +94,6 @@ exports.refreshInviteCode = async (householdId) => {
// Join household via invite code
exports.joinHousehold = async (inviteCode, userId) => {
// Find household by invite code
const householdResult = await pool.query(
`SELECT id, name FROM households
WHERE invite_code = $1
@ -102,22 +101,19 @@ exports.joinHousehold = async (inviteCode, userId) => {
[inviteCode]
);
if (householdResult.rows.length === 0) {
return null; // Invalid or expired code
}
if (householdResult.rows.length === 0) return null;
const household = householdResult.rows[0];
// Check if already member
const existingMember = await pool.query(
`SELECT id FROM household_members
WHERE household_id = $1 AND user_id = $2`,
[household.id, userId]
);
if (existingMember.rows.length > 0) {
return { ...household, alreadyMember: true };
}
if (existingMember.rows.length > 0) return { ...household, alreadyMember: true };
// Add as user role
await pool.query(

74
docs/README.md Normal file
View File

@ -0,0 +1,74 @@
# Documentation Index
This directory contains all project documentation organized by category.
## 📁 Directory Structure
### `/architecture` - System Design & Structure
- **[component-structure.md](architecture/component-structure.md)** - Frontend component organization and patterns
- **[multi-household-architecture-plan.md](architecture/multi-household-architecture-plan.md)** - Multi-household system architecture design
### `/features` - Feature Implementation Details
- **[classification-implementation.md](features/classification-implementation.md)** - Item classification system (zones, types, groups)
- **[image-storage-implementation.md](features/image-storage-implementation.md)** - Image storage and handling (bytea, MIME types)
### `/guides` - How-To & Reference Guides
- **[api-documentation.md](guides/api-documentation.md)** - REST API endpoints and usage
- **[frontend-readme.md](guides/frontend-readme.md)** - Frontend development guide
- **[MOBILE_RESPONSIVE_AUDIT.md](guides/MOBILE_RESPONSIVE_AUDIT.md)** - Mobile-first design guidelines and audit checklist
- **[setup-checklist.md](guides/setup-checklist.md)** - Development environment setup steps
### `/migration` - Database Migrations & Updates
- **[MIGRATION_GUIDE.md](migration/MIGRATION_GUIDE.md)** - Multi-household migration instructions (also in `backend/migrations/`)
- **[POST_MIGRATION_UPDATES.md](migration/POST_MIGRATION_UPDATES.md)** - Required updates after migration
### `/archive` - Completed Implementation Records
Historical documentation of completed features. Useful for reference but not actively maintained.
- **[ACCOUNT_MANAGEMENT_IMPLEMENTATION.md](archive/ACCOUNT_MANAGEMENT_IMPLEMENTATION.md)** - Phase 4: Display name and password change
- **[code-cleanup-guide.md](archive/code-cleanup-guide.md)** - Code cleanup checklist (completed)
- **[HOUSEHOLD_MANAGEMENT_IMPLEMENTATION.md](archive/HOUSEHOLD_MANAGEMENT_IMPLEMENTATION.md)** - Household management UI implementation
- **[IMPLEMENTATION_STATUS.md](archive/IMPLEMENTATION_STATUS.md)** - Multi-household migration sprint status
- **[settings-dark-mode.md](archive/settings-dark-mode.md)** - Dark mode implementation notes
- **[TEST_SUITE_README.md](archive/TEST_SUITE_README.md)** - Testing infrastructure documentation
---
## 📄 Root-Level Documentation
These files remain at the project root for easy access:
- **[../README.md](../README.md)** - Project overview and quick start
- **[../.github/copilot-instructions.md](../.github/copilot-instructions.md)** - AI assistant instructions (architecture, RBAC, conventions)
---
## 🔍 Quick Reference
**Setting up the project?** → Start with [setup-checklist.md](guides/setup-checklist.md)
**Understanding the API?** → See [api-documentation.md](guides/api-documentation.md)
**Working on mobile UI?** → Check [MOBILE_RESPONSIVE_AUDIT.md](guides/MOBILE_RESPONSIVE_AUDIT.md)
**Need architecture context?** → Read [../.github/copilot-instructions.md](../.github/copilot-instructions.md)
**Running migrations?** → Follow [MIGRATION_GUIDE.md](migration/MIGRATION_GUIDE.md)
---
## 📝 Contributing to Documentation
When adding new documentation:
1. **Guides** (`/guides`) - General how-to, setup, reference
2. **Features** (`/features`) - Specific feature implementation details
3. **Architecture** (`/architecture`) - System design, patterns, structure
4. **Migration** (`/migration`) - Database migrations and upgrade guides
5. **Archive** (`/archive`) - Completed implementation records (for reference only)
Keep documentation:
- ✅ Up-to-date with code changes
- ✅ Concise and scannable
- ✅ Linked to relevant files (use relative paths)
- ✅ Organized by category

View File

@ -0,0 +1,43 @@
# API Test Suite
The test suite has been reorganized into separate files for better maintainability:
## New Modular Structure (✅ Complete)
- **api-tests.html** - Main HTML file
- **test-config.js** - Global state management
- **test-definitions.js** - All 62 test cases across 8 categories
- **test-runner.js** - Test execution logic
- **test-ui.js** - UI manipulation functions
- **test-styles.css** - All CSS styles
## How to Use
1. Start the dev server: `docker-compose -f docker-compose.dev.yml up`
2. Navigate to: `http://localhost:5000/test/api-tests.html`
3. Configure credentials (default: admin/admin123)
4. Click "▶ Run All Tests"
## Features
- ✅ 62 comprehensive tests
- ✅ Collapsible test cards (collapsed by default)
- ✅ Expected field validation with visual indicators
- ✅ Color-coded HTTP status badges
- ✅ Auto-expansion on test run
- ✅ Expand/Collapse all buttons
- ✅ Real-time pass/fail/error states
- ✅ Summary dashboard
## File Structure
```
backend/public/
├── api-tests.html # Main entry point (use this)
├── test-config.js # State management (19 lines)
├── test-definitions.js # Test cases (450+ lines)
├── test-runner.js # Test execution (160+ lines)
├── test-ui.js # UI functions (90+ lines)
└── test-styles.css # All styles (310+ lines)
```
## Old File
- **api-test.html** - Original monolithic version (kept for reference)
Total: ~1030 lines split into 6 clean, modular files

View File

@ -0,0 +1,283 @@
# Mobile Responsive Design Audit & Recommendations
## ✅ Already Mobile-Friendly
### Components
1. **Navbar** - Just updated with hamburger menu, dropdowns, sticky positioning
2. **AdminPanel** - Has responsive breakpoints (768px, 480px)
3. **Manage page** - Has responsive breakpoints (768px, 480px)
4. **ManageHousehold** - Has 768px breakpoint
5. **Settings** - Has 768px breakpoint
6. **StoreManagement** - Has 768px breakpoint
7. **GroceryList** - Has 480px breakpoint
## ✅ Recently Completed (2026-01-26)
### **All Modals** - Mobile optimization COMPLETE ✓
**Files updated with responsive styles:**
- ✅ `frontend/src/styles/AddImageModal.css` - Added 768px & 480px breakpoints
- ✅ `frontend/src/styles/ImageUploadModal.css` - Added 768px & 480px breakpoints
- ✅ `frontend/src/styles/ItemClassificationModal.css` - Added 768px & 480px breakpoints
- ✅ `frontend/src/styles/SimilarItemModal.css` - Added 768px & 480px breakpoints
- ✅ `frontend/src/styles/components/EditItemModal.css` - Added 768px & 480px breakpoints
- ✅ `frontend/src/styles/components/ConfirmAddExistingModal.css` - Added 768px & 480px breakpoints
- ✅ `frontend/src/styles/ImageModal.css` - Enhanced with 480px breakpoint
- ✅ `frontend/src/styles/components/AddItemWithDetailsModal.css` - Enhanced with 768px breakpoint
- ✅ `frontend/src/styles/ConfirmBuyModal.css` - Already excellent (480px & 360px breakpoints)
**Mobile improvements implemented:**
- Modal width: 95% at 768px, 100% at 480px
- All buttons: Full-width stacking on mobile with 44px minimum height
- Input fields: 16px font-size to prevent iOS zoom
- Image previews: Responsive sizing (180-200px on mobile)
- Touch targets: 44x44px minimum for all interactive elements
- Overflow: Auto scrolling for tall modals (max-height: 90vh)
- Spacing: Reduced padding on small screens
## ⚠️ Needs Improvement
### High Priority
#### 1. **HouseholdSwitcher** - Dropdown might overflow on mobile
**File:** `frontend/src/styles/components/HouseholdSwitcher.css`
**Current:** No mobile breakpoints
**Needs:**
```css
@media (max-width: 480px) {
.household-switcher-dropdown {
max-width: 90vw;
right: auto;
left: 50%;
transform: translateX(-50%);
}
}
```
#### 2. **StoreTabs** - Horizontal scrolling tabs on mobile
**File:** `frontend/src/styles/components/StoreTabs.css`
**Needs:**
```css
@media (max-width: 768px) {
.store-tabs {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.store-tab {
min-width: 100px;
font-size: 0.9rem;
padding: 0.6rem 1rem;
}
}
```
#### 3. **Login/Register Pages** - Need better mobile padding
**Files:**
- `frontend/src/styles/pages/Login.css`
- `frontend/src/styles/pages/Register.css`
**Needs:**
```css
@media (max-width: 480px) {
.card {
padding: 1.5rem 1rem;
margin: 0.5rem;
}
.form-input {
font-size: 16px; /* Prevents iOS zoom on focus */
}
}
```
### Medium Priority
#### 4. **GroceryList Item Cards** - Could be more touch-friendly
**File:** `frontend/src/styles/pages/GroceryList.css`
**Current:** Has 480px breakpoint
**Enhancement needed:**
- Increase touch target sizes for mobile
- Better spacing between items on small screens
- Optimize image display on mobile
#### 5. **AddItemForm** - Input width and spacing
**File:** `frontend/src/styles/components/AddItemForm.css`
**Has 480px breakpoint** but verify:
- Input font-size is 16px+ (prevents iOS zoom)
- Buttons are full-width on mobile
- Adequate spacing between form elements
#### 6. **CreateJoinHousehold Modal**
**File:** `frontend/src/styles/components/manage/CreateJoinHousehold.css`
**Has 600px breakpoint** - Review for:
- Full-screen on very small devices
- Button sizing and spacing
- Tab navigation usability
### Low Priority
#### 7. **SuggestionList** - Touch interactions
**File:** `frontend/src/styles/components/SuggestionList.css`
**Needs:** Mobile-specific styles for:
- Larger tap targets
- Better scrolling behavior
- Touch feedback
#### 8. **ClassificationSection** - Zone selection on mobile
**File:** `frontend/src/styles/components/ClassificationSection.css`
**Needs:**
- Ensure zone buttons are touch-friendly
- Stack vertically if needed on small screens
#### 9. **ImageUploadSection**
**File:** `frontend/src/styles/components/ImageUploadSection.css`
**Needs:**
- Camera access optimization for mobile
- Preview image sizing
- Upload button sizing
## 🎯 General Recommendations
### 1. **Global Styles**
Update `frontend/src/index.css`:
```css
/* Prevent zoom on input focus (iOS) */
input, select, textarea {
font-size: 16px;
}
/* Better touch scrolling */
* {
-webkit-overflow-scrolling: touch;
}
/* Ensure body doesn't overflow horizontally */
body {
overflow-x: hidden;
}
```
### 2. **Container Max-Widths**
Standardize across the app:
- Small components: `max-width: 600px`
- Medium pages: `max-width: 800px`
- Wide layouts: `max-width: 1200px`
- Always pair with `margin: 0 auto` and `padding: 1rem`
### 3. **Button Sizing**
Mobile-friendly buttons:
```css
.btn-primary, .btn-secondary {
min-height: 44px; /* Apple's recommended minimum */
padding: 0.75rem 1.5rem;
}
@media (max-width: 768px) {
.btn-primary, .btn-secondary {
width: 100%;
margin-bottom: 0.5rem;
}
}
```
### 4. **Form Layouts**
Stack form fields on mobile:
```css
.form-row {
display: flex;
gap: 1rem;
}
@media (max-width: 768px) {
.form-row {
flex-direction: column;
}
}
```
### 5. **Image Handling**
Responsive images:
```css
img {
max-width: 100%;
height: auto;
}
```
### 6. **Typography**
Adjust for mobile readability:
```css
@media (max-width: 768px) {
h1 { font-size: 1.75rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; }
body { font-size: 16px; } /* Prevents iOS zoom */
}
```
## 📱 Testing Checklist
Test on these viewports:
- [ ] 320px (iPhone SE)
- [ ] 375px (iPhone 12/13 Pro)
- [ ] 390px (iPhone 14 Pro)
- [ ] 414px (iPhone Pro Max)
- [ ] 768px (iPad Portrait)
- [ ] 1024px (iPad Landscape)
- [ ] 1280px+ (Desktop)
Test these interactions:
- [ ] Navigation menu (hamburger)
- [ ] Dropdowns (household, user menu)
- [ ] All modals
- [ ] Form inputs (no zoom on focus)
- [ ] Touch gestures (swipe, long-press)
- [ ] Scrolling (no horizontal overflow)
- [ ] Image upload/viewing
- [ ] Tab navigation
## 🔄 Future Considerations
1. **Progressive Web App (PWA)**
- Add manifest.json
- Service worker for offline support
- Install prompt
2. **Touch Gestures**
- Swipe to delete items
- Pull to refresh lists
- Long-press for context menu
3. **Keyboard Handling**
- iOS keyboard overlap handling
- Android keyboard behavior
- Input focus management
4. **Performance**
- Lazy load images
- Virtual scrolling for long lists
- Code splitting by route
## 📝 How to Maintain Mobile-First Design
I've updated `.github/copilot-instructions.md` with mobile-first design principles. This will be included in all future conversations automatically.
**To ensure I remember in new conversations:**
1. ✅ Mobile-first guidelines are now in copilot-instructions.md (automatically loaded)
2. Start conversations with: "Remember to keep mobile/desktop responsiveness in mind"
3. Review this audit document before making UI changes
4. Run mobile testing after any CSS/layout changes
**Quick reminder phrases:**
- "Make this mobile-friendly"
- "Add responsive breakpoints"
- "Test on mobile viewports"
- "Ensure touch-friendly targets"

View File

@ -0,0 +1,243 @@
# Multi-Household Architecture Migration Guide
## Pre-Migration Checklist
- [ ] **Backup Database**
```bash
pg_dump -U your_user -d grocery_list > backup_$(date +%Y%m%d_%H%M%S).sql
```
- [ ] **Test on Staging First**
- Copy production database to staging environment
- Run migration on staging
- Verify all data migrated correctly
- Test application functionality
- [ ] **Review Migration Script**
- Read through `multi_household_architecture.sql`
- Understand each step
- Note verification queries
- [ ] **Announce Maintenance Window**
- Notify users of downtime
- Schedule during low-usage period
- Estimate 15-30 minutes for migration
## Running the Migration
### 1. Connect to Database
```bash
psql -U your_user -d grocery_list
```
### 2. Run Migration
```sql
\i backend/migrations/multi_household_architecture.sql
```
The script will:
1. ✅ Create 8 new tables
2. ✅ Create default "Main Household"
3. ✅ Create default "Costco" store
4. ✅ Migrate all users to household members
5. ✅ Extract items to master catalog
6. ✅ Migrate grocery_list → household_lists
7. ✅ Migrate classifications
8. ✅ Migrate history records
9. ✅ Update user system roles
### 3. Verify Migration
Run these queries inside psql:
```sql
-- Check household created
SELECT * FROM households;
-- Check all users migrated
SELECT u.username, u.role as system_role, hm.role as household_role
FROM users u
JOIN household_members hm ON u.id = hm.user_id
ORDER BY u.id;
-- Check item counts match
SELECT
(SELECT COUNT(DISTINCT item_name) FROM grocery_list) as old_unique_items,
(SELECT COUNT(*) FROM items) as new_items;
-- Check list counts
SELECT
(SELECT COUNT(*) FROM grocery_list) as old_lists,
(SELECT COUNT(*) FROM household_lists) as new_lists;
-- Check classification counts
SELECT
(SELECT COUNT(*) FROM item_classification) as old_classifications,
(SELECT COUNT(*) FROM household_item_classifications) as new_classifications;
-- Check history counts
SELECT
(SELECT COUNT(*) FROM grocery_history) as old_history,
(SELECT COUNT(*) FROM household_list_history) as new_history;
-- Verify no data loss - check if all old items have corresponding new records
SELECT gl.item_name
FROM grocery_list gl
LEFT JOIN items i ON LOWER(i.name) = LOWER(TRIM(gl.item_name))
LEFT JOIN household_lists hl ON hl.item_id = i.id
WHERE hl.id IS NULL;
-- Should return 0 rows
-- Check invite code
SELECT name, invite_code FROM households;
```
### 4. Test Application
- [ ] Users can log in
- [ ] Can view "Main Household" list
- [ ] Can add items
- [ ] Can mark items as bought
- [ ] History shows correctly
- [ ] Classifications preserved
- [ ] Images display correctly
## Post-Migration Cleanup
**Only after verifying everything works correctly:**
```sql
-- Drop old tables (CAREFUL - THIS IS IRREVERSIBLE)
DROP TABLE IF EXISTS grocery_history CASCADE;
DROP TABLE IF EXISTS item_classification CASCADE;
DROP TABLE IF EXISTS grocery_list CASCADE;
```
## Rollback Plan
### If Migration Fails
```sql
-- Inside psql during migration
ROLLBACK;
-- Then restore from backup
\q
psql -U your_user -d grocery_list < backup_YYYYMMDD_HHMMSS.sql
```
### If Issues Found After Migration
```bash
# Drop the database and restore
dropdb grocery_list
createdb grocery_list
psql -U your_user -d grocery_list < backup_YYYYMMDD_HHMMSS.sql
```
## Common Issues & Solutions
### Issue: Duplicate items in items table
**Cause**: Case-insensitive matching not working
**Solution**: Check item names for leading/trailing spaces
### Issue: Foreign key constraint errors
**Cause**: User or item references not found
**Solution**: Verify all users and items exist before migrating lists
### Issue: History not showing
**Cause**: household_list_id references incorrect
**Solution**: Check JOIN conditions in history migration
### Issue: Images not displaying
**Cause**: BYTEA encoding issues
**Solution**: Verify image_mime_type correctly migrated
## Migration Timeline
- **T-0**: Begin maintenance window
- **T+2min**: Backup complete
- **T+3min**: Start migration script
- **T+8min**: Migration complete (for ~1000 items)
- **T+10min**: Run verification queries
- **T+15min**: Test application functionality
- **T+20min**: If successful, announce completion
- **T+30min**: End maintenance window
## Data Integrity Checks
```sql
-- Ensure all users belong to at least one household
SELECT u.id, u.username
FROM users u
LEFT JOIN household_members hm ON u.id = hm.user_id
WHERE hm.id IS NULL;
-- Should return 0 rows
-- Ensure all household lists have valid items
SELECT hl.id
FROM household_lists hl
LEFT JOIN items i ON hl.item_id = i.id
WHERE i.id IS NULL;
-- Should return 0 rows
-- Ensure all history has valid list references
SELECT hlh.id
FROM household_list_history hlh
LEFT JOIN household_lists hl ON hlh.household_list_id = hl.id
WHERE hl.id IS NULL;
-- Should return 0 rows
-- Check for orphaned classifications
SELECT hic.id
FROM household_item_classifications hic
LEFT JOIN household_lists hl ON hic.item_id = hl.item_id
AND hic.household_id = hl.household_id
AND hic.store_id = hl.store_id
WHERE hl.id IS NULL;
-- Should return 0 rows (or classifications for removed items, which is ok)
```
## Success Criteria
✅ All tables created successfully
✅ All users migrated to "Main Household"
✅ Item count matches (unique items from old → new)
✅ List count matches (all grocery_list items → household_lists)
✅ Classification count matches
✅ History count matches
✅ No NULL foreign keys
✅ Application loads without errors
✅ Users can perform all CRUD operations
✅ Images display correctly
✅ Bought items still marked as bought
✅ Recently bought still shows correctly
## Next Steps After Migration
1. ✅ Update backend models (Sprint 2)
2. ✅ Update API routes
3. ✅ Update controllers
4. ✅ Test all endpoints
5. ✅ Update frontend contexts
6. ✅ Update UI components
7. ✅ Enable multi-household features
## Support & Troubleshooting
If issues arise:
1. Check PostgreSQL logs: `/var/log/postgresql/`
2. Check application logs
3. Restore from backup if needed
4. Review migration script for errors
## Monitoring Post-Migration
For the first 24 hours after migration:
- Monitor error logs
- Watch for performance issues
- Verify user activity normal
- Check for any data inconsistencies
- Be ready to rollback if critical issues found

View File

@ -15,7 +15,7 @@ export const getHouseholdStores = (householdId) =>
* Add a store to a household
*/
export const addStoreToHousehold = (householdId, storeId, isDefault = false) =>
api.post(`/stores/household/${householdId}`, { store_id: storeId, is_default: isDefault });
api.post(`/stores/household/${householdId}`, { storeId: storeId, isDefault: isDefault });
/**
* Remove a store from a household

View File

@ -1,35 +1,84 @@
import "../../styles/components/Navbar.css";
import { useContext } from "react";
import { useContext, useState } from "react";
import { Link } from "react-router-dom";
import { AuthContext } from "../../context/AuthContext";
import HouseholdSwitcher from "../household/HouseholdSwitcher";
export default function Navbar() {
const { role, logout, username } = useContext(AuthContext);
const [showNavMenu, setShowNavMenu] = useState(false);
const [showUserMenu, setShowUserMenu] = useState(false);
const closeMenus = () => {
setShowNavMenu(false);
setShowUserMenu(false);
};
return (
<nav className="navbar">
<div className="navbar-links">
<Link to="/">Home</Link>
<Link to="/manage">Manage</Link>
<Link to="/settings">Settings</Link>
{/* Left: Navigation Menu */}
<div className="navbar-section navbar-left">
<button
className="navbar-menu-btn"
onClick={() => {
setShowNavMenu(!showNavMenu);
setShowUserMenu(false);
}}
aria-label="Navigation menu"
>
<span className="hamburger-icon">
<span></span>
<span></span>
<span></span>
</span>
</button>
{role === "system_admin" && <Link to="/admin">Admin</Link>}
{showNavMenu && (
<>
<div className="menu-overlay" onClick={closeMenus}></div>
<div className="navbar-dropdown nav-dropdown">
<Link to="/" onClick={closeMenus}>Home</Link>
<Link to="/manage" onClick={closeMenus}>Manage</Link>
<Link to="/settings" onClick={closeMenus}>Settings</Link>
{role === "system_admin" && <Link to="/admin" onClick={closeMenus}>Admin</Link>}
</div>
</>
)}
</div>
{/* Center: Household Switcher */}
<div className="navbar-section navbar-center">
<HouseholdSwitcher />
<div className="navbar-idcard">
<div className="navbar-idinfo">
<span className="navbar-username">{username}</span>
<span className="navbar-role">{role}</span>
</div>
</div>
<button className="navbar-logout" onClick={logout}>
{/* Right: User Menu */}
<div className="navbar-section navbar-right">
<button
className="navbar-user-btn"
onClick={() => {
setShowUserMenu(!showUserMenu);
setShowNavMenu(false);
}}
>
{username}
</button>
{showUserMenu && (
<>
<div className="menu-overlay" onClick={closeMenus}></div>
<div className="navbar-dropdown user-dropdown">
<div className="user-dropdown-info">
<span className="user-dropdown-username">{username}</span>
<span className="user-dropdown-role">{role}</span>
</div>
<button className="user-dropdown-logout" onClick={() => { logout(); closeMenus(); }}>
Logout
</button>
</div>
</>
)}
</div>
</nav>
);
}

View File

@ -11,7 +11,7 @@ import "../../styles/components/manage/ManageStores.css";
export default function ManageStores() {
const { activeHousehold } = useContext(HouseholdContext);
const { stores: householdStores, loadStores } = useContext(StoreContext);
const { stores: householdStores, refreshStores } = useContext(StoreContext);
const [allStores, setAllStores] = useState([]);
const [loading, setLoading] = useState(true);
const [showAddStore, setShowAddStore] = useState(false);
@ -36,8 +36,9 @@ export default function ManageStores() {
const handleAddStore = async (storeId) => {
try {
console.log("Adding store with ID:", storeId);
await addStoreToHousehold(activeHousehold.id, storeId, false);
await loadStores();
await refreshStores();
setShowAddStore(false);
} catch (error) {
console.error("Failed to add store:", error);
@ -50,7 +51,7 @@ export default function ManageStores() {
try {
await removeStoreFromHousehold(activeHousehold.id, storeId);
await loadStores();
await refreshStores();
} catch (error) {
console.error("Failed to remove store:", error);
alert("Failed to remove store");
@ -60,7 +61,7 @@ export default function ManageStores() {
const handleSetDefault = async (storeId) => {
try {
await setDefaultStore(activeHousehold.id, storeId);
await loadStores();
await refreshStores();
} catch (error) {
console.error("Failed to set default store:", error);
alert("Failed to set default store");

View File

@ -81,3 +81,38 @@
.add-image-remove:hover {
background: rgba(255, 0, 0, 1);
}
/* Mobile Responsive Styles */
@media (max-width: 768px) {
.add-image-preview-container {
margin: 1rem 0;
}
.add-image-preview {
width: 200px;
height: 200px;
}
.add-image-option-btn {
padding: 1rem;
font-size: 1rem;
min-height: 44px;
}
}
@media (max-width: 480px) {
.add-image-preview {
width: 180px;
height: 180px;
}
.add-image-option-btn {
font-size: 0.95rem;
}
.add-image-remove {
width: 36px;
height: 36px;
font-size: 1.3em;
}
}

View File

@ -64,3 +64,20 @@
}
}
@media (max-width: 480px) {
.image-modal-overlay {
padding: 0.5rem;
}
.image-modal-content {
padding: 0.5rem;
border-radius: 8px;
max-width: 95vw;
}
.image-modal-img {
max-height: 50vh;
border-radius: 4px;
}
}

View File

@ -201,3 +201,67 @@
opacity: 1;
}
}
/* Mobile Responsive Styles */
@media (max-width: 768px) {
.image-upload-modal {
width: 95%;
padding: 1.5rem;
max-height: 90vh;
overflow-y: auto;
}
.image-upload-modal h2 {
font-size: 1.3em;
}
.image-upload-option-btn {
padding: 1rem;
font-size: 1rem;
min-height: 44px;
}
.modal-image-preview {
width: 180px;
height: 180px;
}
.image-upload-actions {
flex-direction: column;
gap: 0.75rem;
}
.image-upload-cancel,
.image-upload-skip,
.image-upload-confirm {
width: 100%;
min-height: 44px;
}
}
@media (max-width: 480px) {
.image-upload-modal {
width: 100%;
padding: 1rem;
border-radius: 8px;
}
.image-upload-modal h2 {
font-size: 1.15em;
}
.image-upload-subtitle {
font-size: 0.9em;
}
.modal-image-preview {
width: 160px;
height: 160px;
}
.modal-remove-image {
width: 36px;
height: 36px;
font-size: 1.3em;
}
}

View File

@ -100,3 +100,52 @@
.classification-modal-btn-confirm:hover {
background: #0056b3;
}
/* Mobile Responsive Styles */
@media (max-width: 768px) {
.classification-modal-content {
width: 95%;
padding: 1.25rem;
max-height: 85vh;
overflow-y: auto;
}
.classification-modal-title {
font-size: 1.3em;
}
.classification-modal-subtitle {
font-size: 0.9em;
}
.classification-modal-select {
padding: 0.7em;
font-size: 16px; /* Prevents iOS zoom */
}
.classification-modal-actions {
flex-direction: column;
gap: 0.6rem;
}
.classification-modal-btn {
width: 100%;
min-height: 44px;
}
}
@media (max-width: 480px) {
.classification-modal-content {
width: 100%;
padding: 1rem;
border-radius: 8px;
}
.classification-modal-title {
font-size: 1.2em;
}
.classification-modal-field label {
font-size: 0.9em;
}
}

View File

@ -22,3 +22,23 @@
width: 100%;
}
/* Mobile Responsive Styles */
@media (max-width: 768px) {
.similar-item-suggested,
.similar-item-original {
font-size: 1em;
}
.similar-modal-actions .btn {
min-height: 44px;
font-size: 1rem;
}
}
@media (max-width: 480px) {
.similar-item-suggested,
.similar-item-original {
font-size: 0.95em;
}
}

View File

@ -195,13 +195,61 @@
}
/* Mobile responsiveness */
@media (max-width: 480px) {
@media (max-width: 768px) {
.add-item-details-overlay {
padding: var(--spacing-sm);
}
.add-item-details-modal {
padding: 1.2em;
width: 95%;
max-width: 95%;
padding: var(--spacing-md);
}
.add-item-details-title {
font-size: 1.2em;
font-size: 1.3em;
}
.add-item-details-select {
padding: 0.7em;
font-size: 16px; /* Prevents iOS zoom */
}
.add-item-details-image-options {
gap: 0.6em;
}
.add-item-details-image-btn {
min-height: 44px;
}
.add-item-details-actions {
flex-direction: column;
gap: 0.5rem;
}
.add-item-details-btn {
width: 100%;
min-height: 44px;
}
}
@media (max-width: 480px) {
.add-item-details-modal {
padding: 1rem;
border-radius: 8px;
}
.add-item-details-title {
font-size: 1.15em;
}
.add-item-details-subtitle {
font-size: 0.85em;
}
.add-item-details-section-title {
font-size: 1em;
}
.add-item-details-image-options {
@ -210,9 +258,10 @@
.add-item-details-image-btn {
min-width: 100%;
font-size: 0.9em;
}
.add-item-details-actions {
flex-direction: column;
.add-item-details-field label {
font-size: 0.85em;
}
}

View File

@ -39,3 +39,30 @@
font-size: var(--font-size-lg);
}
/* Mobile Responsive Styles */
@media (max-width: 768px) {
.confirm-add-existing-qty-info {
padding: var(--spacing-sm);
}
.qty-row {
font-size: 0.95em;
}
.qty-value {
font-size: 1em;
}
.qty-total .qty-label,
.qty-total .qty-value {
font-size: 1em;
}
}
@media (max-width: 480px) {
.qty-row {
font-size: 0.9em;
padding: var(--spacing-xxs) 0;
}
}

View File

@ -187,3 +187,83 @@
opacity: 0.6;
cursor: not-allowed;
}
/* Mobile Responsive Styles */
@media (max-width: 768px) {
.edit-modal-overlay {
padding: var(--spacing-sm);
}
.edit-modal-content {
width: 95%;
max-width: 95%;
padding: var(--spacing-md);
max-height: 90vh;
}
.edit-modal-title {
font-size: 1.3em;
}
.edit-modal-input,
.edit-modal-select {
font-size: 16px; /* Prevents iOS zoom */
}
.edit-modal-quantity-input {
width: 70px;
font-size: 16px; /* Prevents iOS zoom */
}
.quantity-btn {
width: 44px;
height: 44px;
font-size: 1.4em;
}
.edit-modal-inline-field {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-xs);
}
.edit-modal-inline-field label {
min-width: auto;
}
.edit-modal-inline-field .edit-modal-select {
width: 100%;
}
.edit-modal-actions {
flex-direction: column;
gap: var(--spacing-xs);
}
.edit-modal-btn {
width: 100%;
min-height: 44px;
}
}
@media (max-width: 480px) {
.edit-modal-content {
width: 100%;
padding: 1rem;
border-radius: 8px;
}
.edit-modal-title {
font-size: 1.2em;
}
.edit-modal-quantity-control {
gap: var(--spacing-xs);
}
.quantity-btn {
width: 40px;
height: 40px;
font-size: 1.2em;
}
}

View File

@ -6,6 +6,7 @@
.household-switcher-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--card-bg);
@ -15,6 +16,7 @@
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
}
.household-switcher-toggle:hover {
@ -29,11 +31,18 @@
.household-name {
font-weight: 500;
flex: 1;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropdown-icon {
font-size: 0.75rem;
transition: transform 0.2s ease;
flex-shrink: 0;
margin-left: auto;
}
.dropdown-icon.open {
@ -53,7 +62,8 @@
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
min-width: 220px;
right: 0;
width: 100%;
background: var(--card-bg);
border: 2px solid var(--border);
border-radius: 8px;

View File

@ -1,58 +1,232 @@
/* Navbar - Sticky at top */
.navbar {
position: sticky;
top: 0;
z-index: 1000;
background: #343a40;
color: white;
padding: 0.6em 1em;
padding: 0.75rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 4px;
margin-bottom: 1em;
gap: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.navbar-links a {
/* Navbar Sections */
.navbar-section {
display: flex;
align-items: center;
}
.navbar-left {
flex: 0 0 auto;
}
.navbar-center {
flex: 1 1 auto;
display: flex;
justify-content: center;
max-width: 80%;
margin: 0 auto;
}
.navbar-right {
flex: 0 0 auto;
position: relative;
}
/* Hamburger Menu Button */
.navbar-menu-btn {
background: transparent;
border: none;
cursor: pointer;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.hamburger-icon {
display: flex;
flex-direction: column;
gap: 4px;
width: 24px;
}
.hamburger-icon span {
display: block;
width: 100%;
height: 3px;
background: white;
border-radius: 2px;
transition: all 0.3s;
}
.navbar-menu-btn:hover .hamburger-icon span {
background: #ddd;
}
/* User Button */
.navbar-user-btn {
background: #495057;
color: white;
margin-right: 1em;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
white-space: nowrap;
transition: background 0.2s;
}
.navbar-user-btn:hover {
background: #5a6268;
}
/* Dropdown Overlay */
.menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
background: transparent;
}
/* Dropdown Base Styles */
.navbar-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
background: #fff;
border: 1px solid #dee2e6;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1001;
min-width: 180px;
overflow: hidden;
}
/* Navigation Dropdown */
.nav-dropdown {
left: 0;
display: flex;
flex-direction: column;
}
.nav-dropdown a {
color: #343a40;
text-decoration: none;
font-size: 1.1em;
padding: 0.75rem 1.25rem;
font-size: 1rem;
transition: background 0.2s;
border-bottom: 1px solid #f0f0f0;
}
.navbar-links a:hover {
text-decoration: underline;
.nav-dropdown a:last-child {
border-bottom: none;
}
.navbar-logout {
.nav-dropdown a:hover {
background: #f8f9fa;
color: var(--color-primary);
}
/* User Dropdown */
.user-dropdown {
right: 0;
min-width: 200px;
}
.user-dropdown-info {
padding: 1rem 1.25rem;
border-bottom: 1px solid #dee2e6;
background: #f8f9fa;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.user-dropdown-username {
font-size: 1rem;
font-weight: 600;
color: #343a40;
}
.user-dropdown-role {
font-size: 0.85rem;
color: #6c757d;
text-transform: capitalize;
}
.user-dropdown-logout {
width: 100%;
background: #dc3545;
color: white;
border: none;
padding: 0.4em 0.8em;
border-radius: 4px;
padding: 0.75rem 1.25rem;
cursor: pointer;
width: 100px;
font-size: 1rem;
font-weight: 500;
transition: background 0.2s;
}
.navbar-idcard {
display: flex;
align-items: center;
align-content: center;
margin-right: 1em;
padding: 0.3em 0.6em;
background: #495057;
border-radius: 4px;
color: white;
.user-dropdown-logout:hover {
background: #c82333;
}
.navbar-idinfo {
display: flex;
flex-direction: column;
line-height: 1.1;
/* Household Switcher - Centered with max width */
.navbar-center > * {
width: 100%;
max-width: 24ch; /* 24 characters max width */
}
.navbar-username {
font-size: 0.95em;
font-weight: bold;
/* Mobile Responsive */
@media (max-width: 768px) {
.navbar {
padding: 0.5rem 0.75rem;
gap: 0.5rem;
}
.navbar-role {
font-size: 0.75em;
opacity: 0.8;
.navbar-center {
max-width: 60%;
}
.navbar-user-btn {
padding: 0.4rem 0.75rem;
font-size: 0.9rem;
}
.nav-dropdown {
min-width: 160px;
}
.user-dropdown {
min-width: 180px;
}
}
@media (max-width: 480px) {
.navbar {
padding: 0.5rem;
}
.navbar-center {
max-width: 50%;
}
.navbar-user-btn {
padding: 0.4rem 0.6rem;
font-size: 0.85rem;
}
.hamburger-icon {
width: 20px;
}
.hamburger-icon span {
height: 2.5px;
}
}

View File

@ -12,6 +12,13 @@
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xl);
border-bottom: 2px solid var(--color-border-light);
touch-action: pan-x; /* Lock Y-axis, allow only horizontal scrolling */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.settings-tabs::-webkit-scrollbar {
display: none; /* Chrome/Safari/Opera */
}
.settings-tab {