styling fix and readme files reorg
This commit is contained in:
parent
31eda793ab
commit
11f23eb643
173
.github/copilot-instructions.md
vendored
173
.github/copilot-instructions.md
vendored
@ -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
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
74
docs/README.md
Normal 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
|
||||
43
docs/archive/TEST_SUITE_README.md
Normal file
43
docs/archive/TEST_SUITE_README.md
Normal 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
|
||||
283
docs/guides/MOBILE_RESPONSIVE_AUDIT.md
Normal file
283
docs/guides/MOBILE_RESPONSIVE_AUDIT.md
Normal 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"
|
||||
243
docs/migration/MIGRATION_GUIDE.md
Normal file
243
docs/migration/MIGRATION_GUIDE.md
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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");
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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-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;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-role {
|
||||
font-size: 0.75em;
|
||||
opacity: 0.8;
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user