Compare commits

..

No commits in common. "cd06dbd9fc1a1533f7c25489b35cf3fb4e8ac9be" and "7c58ab57f7bb259542b87ec7082f16816384ac5d" have entirely different histories.

5 changed files with 51 additions and 1549 deletions

View File

@ -1,771 +0,0 @@
# Costco Grocery List API Documentation
Base URL: `http://localhost:5000/api`
## Table of Contents
- [Authentication](#authentication)
- [Grocery List Management](#grocery-list-management)
- [User Management](#user-management)
- [Admin Operations](#admin-operations)
- [Role-Based Access Control](#role-based-access-control)
- [Error Responses](#error-responses)
---
## Authentication
All authenticated endpoints require a JWT token in the `Authorization` header:
```
Authorization: Bearer <token>
```
### POST /auth/register
Register a new user account.
**Access:** Public
**Request Body:**
```json
{
"username": "string (lowercase)",
"password": "string",
"name": "string"
}
```
**Response:** `200 OK`
```json
{
"message": "User registered",
"user": {
"id": 1,
"username": "johndoe",
"name": "John Doe",
"role": "viewer"
}
}
```
**Error:** `400 Bad Request`
```json
{
"message": "Registration failed",
"error": "Error details"
}
```
---
### POST /auth/login
Authenticate and receive a JWT token.
**Access:** Public
**Request Body:**
```json
{
"username": "string (lowercase)",
"password": "string"
}
```
**Response:** `200 OK`
```json
{
"token": "jwt_token_string",
"username": "johndoe",
"role": "editor"
}
```
**Errors:**
- `401 Unauthorized` - User not found or invalid credentials
```json
{
"message": "User not found"
}
```
or
```json
{
"message": "Invalid credentials"
}
```
---
## Grocery List Management
### GET /list
Retrieve all unbought grocery items.
**Access:** Authenticated (All roles)
**Query Parameters:** None
**Response:** `200 OK`
```json
[
{
"id": 1,
"item_name": "milk",
"quantity": 2,
"bought": false,
"item_image": "base64_encoded_string",
"image_mime_type": "image/jpeg",
"added_by_users": ["John Doe", "Jane Smith"],
"modified_on": "2026-01-03T12:00:00Z",
"item_type": "dairy",
"item_group": "milk",
"zone": "DAIRY"
}
]
```
**Fields:**
- `id` - Unique item identifier
- `item_name` - Name of grocery item (lowercase)
- `quantity` - Number of items needed
- `bought` - Purchase status (always false for this endpoint)
- `item_image` - Base64 encoded image (nullable)
- `image_mime_type` - MIME type of image (nullable)
- `added_by_users` - Array of user names who added/modified this item
- `modified_on` - Last modification timestamp
- `item_type` - Classification type (nullable)
- `item_group` - Classification group (nullable)
- `zone` - Store zone location (nullable)
---
### GET /list/item-by-name
Retrieve a specific item by name (case-insensitive).
**Access:** Authenticated (All roles)
**Query Parameters:**
- `itemName` (required) - Name of the item to search
**Example:** `GET /list/item-by-name?itemName=milk`
**Response:** `200 OK`
```json
{
"id": 1,
"item_name": "milk",
"quantity": 2,
"bought": false,
"item_image": "base64_encoded_string",
"image_mime_type": "image/jpeg"
}
```
Returns `null` if item not found.
---
### GET /list/suggest
Get item name suggestions based on partial input.
**Access:** Authenticated (All roles)
**Query Parameters:**
- `query` (required) - Partial item name to search
**Example:** `GET /list/suggest?query=mil`
**Response:** `200 OK`
```json
[
{ "item_name": "milk" },
{ "item_name": "almond milk" },
{ "item_name": "oat milk" }
]
```
**Behavior:**
- Case-insensitive partial matching (ILIKE)
- Searches all items in `grocery_list` (bought and unbought)
- Returns up to 10 suggestions
- Ordered by database query result
---
### GET /list/recently-bought
Retrieve items bought within the last 24 hours.
**Access:** Authenticated (All roles)
**Query Parameters:** None
**Response:** `200 OK`
```json
[
{
"id": 5,
"item_name": "bread",
"quantity": 1,
"bought": true,
"item_image": "base64_encoded_string",
"image_mime_type": "image/jpeg",
"added_by_users": ["John Doe"],
"modified_on": "2026-01-03T10:30:00Z",
"item_type": "bakery",
"item_group": "bread",
"zone": "BAKERY"
}
]
```
**Behavior:**
- Returns items with `bought = true`
- Filters by `modified_on` within last 24 hours
- Includes classification data if available
---
### GET /list/item/:id/classification
Get classification data for a specific item.
**Access:** Authenticated (All roles)
**Path Parameters:**
- `id` (required) - Item ID
**Example:** `GET /list/item/1/classification`
**Response:** `200 OK`
```json
{
"item_type": "dairy",
"item_group": "milk",
"zone": "DAIRY",
"confidence": 1.0,
"source": "user"
}
```
Returns empty object `{}` if no classification exists.
**Fields:**
- `item_type` - Classification type (e.g., "dairy", "produce", "meat")
- `item_group` - Sub-group within type (e.g., "milk", "cheese")
- `zone` - Store zone (e.g., "DAIRY", "PRODUCE", "MEAT")
- `confidence` - Classification confidence (0.0 to 1.0)
- `source` - Origin of classification ("user" or "auto")
---
### POST /list/add
Add a new item or update quantity of existing item.
**Access:** Editor, Admin
**Content-Type:** `multipart/form-data`
**Request Body:**
```
itemName: string (required)
quantity: number (required)
image: file (optional) - Image file
```
**Response:** `200 OK`
```json
{
"message": "Item added/updated",
"addedBy": 1
}
```
**Behavior:**
- If item exists and is unbought: updates quantity
- If item exists and is bought: marks as unbought with new quantity
- If item doesn't exist: creates new item
- Adds record to `grocery_history` table
- Processes and optimizes uploaded image (800x800px, JPEG 85% quality)
**Image Processing:**
- Accepted formats: JPEG, PNG, GIF, WebP
- Max file size: 5MB
- Output: 800x800px JPEG at 85% quality
- Stored as binary in database
---
### POST /list/update-image
Update or add image to an existing item.
**Access:** Editor, Admin
**Content-Type:** `multipart/form-data`
**Request Body:**
```
id: number (required)
itemName: string (required)
quantity: number (required)
image: file (required) - Image file
```
**Response:** `200 OK`
```json
{
"message": "Image updated successfully"
}
```
**Error:** `400 Bad Request`
```json
{
"message": "No image provided"
}
```
---
### POST /list/mark-bought
Mark an item as purchased.
**Access:** Editor, Admin
**Request Body:**
```json
{
"id": 1
}
```
**Response:** `200 OK`
```json
{
"message": "Item marked bought"
}
```
**Behavior:**
- Sets `bought = true`
- Updates `modified_on` timestamp
- Item moves to "Recently Bought" list
---
### PUT /list/item/:id
Update item details and/or classification.
**Access:** Editor, Admin
**Path Parameters:**
- `id` (required) - Item ID
**Request Body:**
```json
{
"itemName": "string (optional)",
"quantity": 2,
"classification": {
"item_type": "dairy",
"item_group": "milk",
"zone": "DAIRY"
}
}
```
**Response:** `200 OK`
```json
{
"message": "Item updated successfully"
}
```
**Errors:**
- `400 Bad Request` - Invalid classification values
```json
{
"message": "Invalid item_type"
}
```
or
```json
{
"message": "Invalid item_group for selected item_type"
}
```
or
```json
{
"message": "Invalid zone"
}
```
**Classification Validation:**
- `item_type`: Must be one of the valid types defined in `backend/constants/classifications.js`
- `item_group`: Must be valid for the selected `item_type`
- `zone`: Must be one of the predefined store zones
**Valid Zones:**
- ENTRANCE, PRODUCE, MEAT, DELI, BAKERY, DAIRY, FROZEN, DRY_GOODS, BEVERAGES, SNACKS, HOUSEHOLD, HEALTH, CHECKOUT
---
## User Management
### GET /users/exists
Check if a username already exists.
**Access:** Public
**Query Parameters:**
- `username` (required) - Username to check
**Example:** `GET /users/exists?username=johndoe`
**Response:** `200 OK`
```json
{
"exists": true
}
```
or
```json
{
"exists": false
}
```
---
### GET /users/test
Health check endpoint for user routes.
**Access:** Public
**Response:** `200 OK`
```json
{
"message": "User route is working"
}
```
---
## Admin Operations
### GET /admin/users
Retrieve all registered users.
**Access:** Admin only
**Query Parameters:** None
**Response:** `200 OK`
```json
[
{
"id": 1,
"username": "johndoe",
"name": "John Doe",
"role": "editor"
},
{
"id": 2,
"username": "janesmith",
"name": "Jane Smith",
"role": "viewer"
}
]
```
---
### PUT /admin/users
Update a user's role.
**Access:** Admin only
**Request Body:**
```json
{
"id": 1,
"role": "admin"
}
```
**Valid Roles:**
- `viewer` - Read-only access
- `editor` - Can add/modify/buy items
- `admin` - Full access including user management
**Response:** `200 OK`
```json
{
"message": "Role updated",
"id": 1,
"role": "admin"
}
```
**Errors:**
- `400 Bad Request` - Invalid role
```json
{
"error": "Invalid role"
}
```
- `404 Not Found` - User not found
```json
{
"error": "User not found"
}
```
- `500 Internal Server Error`
```json
{
"error": "Failed to update role"
}
```
---
### DELETE /admin/users
Delete a user account.
**Access:** Admin only
**Path Parameters:**
- `id` (required) - User ID
**Example:** `DELETE /admin/users?id=5`
**Response:** `200 OK`
```json
{
"message": "User deleted",
"id": 5
}
```
**Errors:**
- `404 Not Found`
```json
{
"error": "User not found"
}
```
- `500 Internal Server Error`
```json
{
"error": "Failed to delete user"
}
```
---
## Role-Based Access Control
### Roles and Permissions
| Role | View Lists | Add/Edit Items | Buy Items | User Management |
|------|-----------|----------------|-----------|-----------------|
| **viewer** | ✅ | ❌ | ❌ | ❌ |
| **editor** | ✅ | ✅ | ✅ | ❌ |
| **admin** | ✅ | ✅ | ✅ | ✅ |
### Protected Endpoints by Role
**All Authenticated Users (viewer, editor, admin):**
- GET /list
- GET /list/item-by-name
- GET /list/suggest
- GET /list/recently-bought
- GET /list/item/:id/classification
**Editor and Admin Only:**
- POST /list/add
- POST /list/update-image
- POST /list/mark-bought
- PUT /list/item/:id
**Admin Only:**
- GET /admin/users
- PUT /admin/users
- DELETE /admin/users
---
## Error Responses
### Standard Error Format
```json
{
"message": "Error description",
"error": "Detailed error information (optional)"
}
```
### Common HTTP Status Codes
| Code | Meaning | Common Causes |
|------|---------|---------------|
| 200 | OK | Successful request |
| 400 | Bad Request | Invalid input data, missing required fields |
| 401 | Unauthorized | Missing/invalid token, invalid credentials |
| 403 | Forbidden | Insufficient permissions for operation |
| 404 | Not Found | Resource doesn't exist |
| 500 | Internal Server Error | Server-side error |
### Authentication Errors
**Missing Token:**
```json
{
"message": "No token provided"
}
```
**Invalid Token:**
```json
{
"message": "Invalid or expired token"
}
```
**Insufficient Permissions:**
```json
{
"message": "Access denied"
}
```
---
## Data Models
### User
```typescript
{
id: number
username: string (lowercase, unique)
password: string (bcrypt hashed)
name: string
role: "viewer" | "editor" | "admin"
}
```
### Grocery Item
```typescript
{
id: number
item_name: string (lowercase)
quantity: number
bought: boolean
item_image: Buffer | null (binary image data)
image_mime_type: string | null
added_by: number (user id)
modified_on: timestamp
}
```
### Item Classification
```typescript
{
id: number (references grocery_list.id)
item_type: string | null
item_group: string | null
zone: string | null
confidence: number (0.0 - 1.0)
source: "user" | "auto"
}
```
### Grocery History
```typescript
{
id: number
list_item_id: number (references grocery_list.id)
quantity: number
added_by: number (user id)
added_on: timestamp
}
```
---
## Notes
### Case Sensitivity
- Usernames are stored and compared in **lowercase**
- Item names are stored in **lowercase**
- All searches are **case-insensitive** (ILIKE in PostgreSQL)
### Token Expiration
- JWT tokens expire after **1 year**
- Include token expiration handling in client applications
### Image Optimization
- Images are automatically processed:
- Resized to 800x800px (maintains aspect ratio)
- Converted to JPEG format
- Compressed to 85% quality
- Max upload size: 5MB
### Database Transaction Handling
- Item additions/updates include history tracking
- Classification updates are atomic operations
### CORS Configuration
- Allowed origins configured via `ALLOWED_ORIGINS` environment variable
- Supports both static origins and IP range patterns (e.g., `192.168.*.*`)
---
## Frontend Integration Examples
### Login Flow
```javascript
const response = await axios.post('/api/auth/login', {
username: 'johndoe',
password: 'password123'
});
const { token, role, username } = response.data;
localStorage.setItem('token', token);
localStorage.setItem('role', role);
localStorage.setItem('username', username);
```
### Authenticated Request
```javascript
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:5000/api',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const items = await api.get('/list');
```
### Adding Item with Image
```javascript
const formData = new FormData();
formData.append('itemName', 'milk');
formData.append('quantity', 2);
formData.append('image', imageFile); // File object from input
await api.post('/list/add', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
```
---
## Version Information
- API Version: 1.0
- Last Updated: January 3, 2026
- Backend Framework: Express 5.1.0
- Database: PostgreSQL 8.16.0
- Authentication: JWT (jsonwebtoken 9.0.2)

774
README.md
View File

@ -1,774 +0,0 @@
# Costco Grocery List
A full-stack web application for managing grocery shopping lists with role-based access control, image support, and intelligent item classification.
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Node](https://img.shields.io/badge/node-20.x-green.svg)
![React](https://img.shields.io/badge/react-19.2.0-blue.svg)
![Express](https://img.shields.io/badge/express-5.1.0-lightgrey.svg)
## 📋 Table of Contents
- [Overview](#overview)
- [System Architecture](#system-architecture)
- [Key Features](#key-features)
- [Technology Stack](#technology-stack)
- [Data Flow](#data-flow)
- [Role-Based Access Control](#role-based-access-control)
- [Getting Started](#getting-started)
- [API Documentation](#api-documentation)
- [Project Structure](#project-structure)
- [Development Workflow](#development-workflow)
- [Deployment](#deployment)
---
## 🎯 Overview
The Costco Grocery List application provides a collaborative platform for managing household grocery shopping. Users can add items with photos, track quantities, mark items as purchased, and organize items by store zones. The system supports multiple users with different permission levels, making it ideal for families or shared households.
**Live Demo:** [https://costco.nicosaya.com](https://costco.nicosaya.com)
---
## 🏗️ System Architecture
```mermaid
graph TB
subgraph "Client Layer"
A[React 19 SPA]
B[Vite Dev Server]
end
subgraph "Application Layer"
C[Express 5 API]
D[JWT Auth Middleware]
E[RBAC Middleware]
F[Image Processing]
end
subgraph "Data Layer"
G[(PostgreSQL)]
H[grocery_list]
I[users]
J[item_classification]
K[grocery_history]
end
A -->|HTTP/REST| C
C --> D
D --> E
E --> F
C --> G
G --> H
G --> I
G --> J
G --> K
style A fill:#61dafb
style C fill:#259dff
style G fill:#336791
```
### Architecture Layers
1. **Presentation Layer (Frontend)**
- React 19 with modern hooks (useState, useEffect, useContext)
- Component-based architecture organized by feature
- CSS custom properties for theming
- Axios for HTTP requests with interceptors
2. **Business Logic Layer (Backend)**
- Express.js REST API
- JWT-based authentication
- Role-based access control (RBAC)
- Image optimization middleware (Sharp)
- Centralized error handling
3. **Data Persistence Layer**
- PostgreSQL relational database
- Normalized schema with foreign key constraints
- Junction table for item history tracking
- Binary storage for optimized images
---
## ✨ Key Features
### 🔐 Authentication & Authorization
- JWT token-based authentication (1 year expiration)
- Three-tier role system (Viewer, Editor, Admin)
- Secure password hashing with bcrypt
- Token-based session management
### 📝 Grocery List Management
- Add items with optional images
- Update item quantities
- Mark items as bought/unbought
- View recently bought items (24-hour window)
- Long-press to edit items (mobile-friendly)
### 🖼️ Image Support
- Upload product images
- Automatic image optimization (800x800px, JPEG 85%)
- Base64 encoding for efficient storage
- 5MB maximum file size
- Support for JPEG, PNG, GIF, WebP
### 🏪 Smart Organization
- Item classification system (type, group, zone)
- 13 predefined store zones
- Sort by zone, alphabetically, or quantity
- Visual grouping by store location
- Intelligent item suggestions
### 🔍 Search & Suggestions
- Real-time autocomplete suggestions
- Fuzzy string matching (80% similarity threshold)
- Substring detection for partial matches
- Case-insensitive search
### 👥 User Management (Admin)
- View all registered users
- Update user roles
- Delete user accounts
- User activity tracking
---
## 🛠️ Technology Stack
### Frontend
| Technology | Version | Purpose |
|------------|---------|---------|
| React | 19.2.0 | UI framework |
| React Router | 7.9.6 | Client-side routing |
| Axios | 1.13.2 | HTTP client |
| Vite | 7.2.2 | Build tool & dev server |
| TypeScript | 5.9.3 | Type safety |
### Backend
| Technology | Version | Purpose |
|------------|---------|---------|
| Node.js | 20.x | Runtime environment |
| Express | 5.1.0 | Web framework |
| PostgreSQL | 8.16.0 | Database |
| JWT | 9.0.2 | Authentication |
| Bcrypt | 3.0.3 | Password hashing |
| Sharp | 0.34.5 | Image processing |
| Multer | 2.0.2 | File upload handling |
### DevOps
| Technology | Purpose |
|------------|---------|
| Docker | Containerization |
| Docker Compose | Multi-container orchestration |
| Gitea Actions | CI/CD pipeline |
| Nginx | Production static file serving |
| Nodemon | Development hot-reload |
---
## 🔄 Data Flow
### Adding an Item
```mermaid
sequenceDiagram
participant User
participant Frontend
participant API
participant Auth
participant RBAC
participant Database
User->>Frontend: Enter item name & quantity
User->>Frontend: Upload image (optional)
Frontend->>API: POST /list/add (FormData)
API->>Auth: Verify JWT token
Auth->>RBAC: Check role (Editor/Admin)
RBAC->>API: Authorization granted
API->>API: Process & optimize image
API->>Database: Check if item exists
alt Item exists & unbought
Database-->>API: Return existing item
API->>Database: UPDATE quantity
else Item exists & bought
Database-->>API: Return existing item
API->>Database: SET bought=false, UPDATE quantity
else Item doesn't exist
API->>Database: INSERT new item
end
API->>Database: INSERT grocery_history record
Database-->>API: Success
API-->>Frontend: 200 OK {message, addedBy}
Frontend-->>User: Show success message
Frontend->>API: GET /list (refresh)
API->>Database: SELECT unbought items
Database-->>API: Return items
API-->>Frontend: Item list
Frontend-->>User: Update UI
```
### Authentication Flow
```mermaid
sequenceDiagram
participant User
participant Frontend
participant API
participant Database
User->>Frontend: Enter credentials
Frontend->>API: POST /auth/login
API->>Database: SELECT user WHERE username
Database-->>API: User record
API->>API: Compare password hash
alt Valid credentials
API->>API: Generate JWT token
API-->>Frontend: {token, role, username}
Frontend->>Frontend: Store token in localStorage
Frontend->>Frontend: Store role in AuthContext
Frontend->>API: GET /list (with token)
API->>API: Verify token
API-->>Frontend: Protected data
else Invalid credentials
API-->>Frontend: 401 Unauthorized
Frontend-->>User: Show error message
end
```
### Item Classification Flow
```mermaid
graph LR
A[Add/Edit Item] --> B{Classification Exists?}
B -->|Yes| C[Display Existing]
B -->|No| D[Show Empty Form]
C --> E[User Edits]
D --> E
E --> F{Validate Classification}
F -->|Valid| G[Upsert to DB]
F -->|Invalid| H[Show Error]
G --> I[confidence=1.0, source='user']
I --> J[Update Item List]
H --> E
style G fill:#90EE90
style H fill:#FFB6C1
```
---
## 🔒 Role-Based Access Control
### Role Hierarchy
```
Admin (Full Access)
├── User Management
├── Item Management
└── View Access
Editor (Modify Access)
├── Add Items
├── Edit Items
├── Mark as Bought
└── View Access
Viewer (Read-Only)
└── View Lists Only
```
### Permission Matrix
| Feature | Viewer | Editor | Admin |
|---------|--------|--------|-------|
| View grocery list | ✅ | ✅ | ✅ |
| View recently bought | ✅ | ✅ | ✅ |
| Get suggestions | ✅ | ✅ | ✅ |
| View classifications | ✅ | ✅ | ✅ |
| Add items | ❌ | ✅ | ✅ |
| Edit items | ❌ | ✅ | ✅ |
| Upload images | ❌ | ✅ | ✅ |
| Mark items bought | ❌ | ✅ | ✅ |
| Update classifications | ❌ | ✅ | ✅ |
| View all users | ❌ | ❌ | ✅ |
| Update user roles | ❌ | ❌ | ✅ |
| Delete users | ❌ | ❌ | ✅ |
### Middleware Chain
Protected routes use a middleware chain pattern:
```javascript
router.post("/add",
auth, // Verify JWT token
requireRole(ROLES.EDITOR, ROLES.ADMIN), // Check role
upload.single("image"), // Handle file upload
processImage, // Optimize image
controller.addItem // Execute business logic
);
```
---
## 🚀 Getting Started
### Prerequisites
- **Node.js** 20.x or higher
- **PostgreSQL** 8.x or higher
- **Docker** (optional, recommended)
- **Git** for version control
### Local Development Setup
1. **Clone the repository**
```bash
git clone https://github.com/your-org/costco-grocery-list.git
cd costco-grocery-list
```
2. **Configure environment variables**
Create `backend/.env`:
```env
# Database Configuration
DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=your_password
DB_DATABASE=grocery_list
DB_PORT=5432
# Authentication
JWT_SECRET=your_secret_key_here
# CORS Configuration
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
# Server
PORT=5000
```
3. **Start with Docker (Recommended)**
```bash
# Development mode with hot-reload
docker-compose -f docker-compose.dev.yml up
# Access application:
# Frontend: http://localhost:3000
# Backend: http://localhost:5000
```
4. **Manual Setup (Alternative)**
**Backend:**
```bash
cd backend
npm install
npm run dev
```
**Frontend:**
```bash
cd frontend
npm install
npm run dev
```
5. **Database Setup**
Create PostgreSQL database and tables:
```sql
CREATE DATABASE grocery_list;
-- See backend/models/*.js for table schemas
```
### First User Registration
The first registered user should be manually promoted to admin:
```sql
UPDATE users SET role = 'admin' WHERE username = 'your_username';
```
---
## 📚 API Documentation
For detailed API documentation including all endpoints, request/response formats, and examples, see [API_DOCUMENTATION.md](./API_DOCUMENTATION.md).
### Quick Reference
**Base URL:** `http://localhost:5000/api`
**Common Endpoints:**
- `POST /auth/register` - Register new user
- `POST /auth/login` - Authenticate user
- `GET /list` - Get all unbought items
- `POST /list/add` - Add or update item
- `POST /list/mark-bought` - Mark item as bought
- `GET /list/recently-bought` - Get items bought in last 24h
- `GET /admin/users` - Get all users (Admin only)
**Authentication:**
All protected endpoints require a JWT token in the Authorization header:
```
Authorization: Bearer <your_jwt_token>
```
---
## 📁 Project Structure
```
costco-grocery-list/
├── .gitea/
│ └── workflows/
│ └── deploy.yml # CI/CD pipeline configuration
├── backend/
│ ├── constants/
│ │ └── classifications.js # Item type/zone definitions
│ ├── controllers/
│ │ ├── auth.controller.js # Authentication logic
│ │ ├── lists.controller.js # Grocery list logic
│ │ └── users.controller.js # User management logic
│ ├── db/
│ │ └── pool.js # PostgreSQL connection pool
│ ├── middleware/
│ │ ├── auth.js # JWT verification
│ │ ├── rbac.js # Role-based access control
│ │ └── image.js # Image upload & processing
│ ├── models/
│ │ ├── list.model.js # Grocery list database queries
│ │ └── user.model.js # User database queries
│ ├── routes/
│ │ ├── auth.routes.js # Authentication routes
│ │ ├── list.routes.js # Grocery list routes
│ │ ├── admin.routes.js # Admin routes
│ │ └── users.routes.js # User routes
│ ├── app.js # Express app configuration
│ ├── server.js # Server entry point
│ ├── Dockerfile # Backend container config
│ └── package.json
├── frontend/
│ ├── public/ # Static assets
│ ├── src/
│ │ ├── api/
│ │ │ ├── axios.js # Axios instance with interceptors
│ │ │ ├── auth.js # Auth API calls
│ │ │ ├── list.js # List API calls
│ │ │ └── users.js # User API calls
│ │ ├── components/
│ │ │ ├── common/ # Reusable components
│ │ │ ├── forms/ # Form components
│ │ │ ├── items/ # Item-related components
│ │ │ ├── layout/ # Layout components
│ │ │ └── modals/ # Modal dialogs
│ │ ├── constants/
│ │ │ └── roles.js # Role constants
│ │ ├── context/
│ │ │ └── AuthContext.jsx # Authentication context
│ │ ├── pages/
│ │ │ ├── AdminPanel.jsx # Admin user management
│ │ │ ├── GroceryList.jsx # Main grocery list page
│ │ │ ├── Login.jsx # Login page
│ │ │ └── Register.jsx # Registration page
│ │ ├── styles/
│ │ │ ├── theme.css # CSS custom properties
│ │ │ ├── components/ # Component-specific styles
│ │ │ └── pages/ # Page-specific styles
│ │ ├── utils/
│ │ │ ├── PrivateRoute.jsx # Route protection
│ │ │ ├── RoleGuard.jsx # Role-based component guard
│ │ │ └── stringSimilarity.js # Fuzzy matching algorithm
│ │ ├── App.jsx # Root component with routing
│ │ └── main.tsx # Application entry point
│ ├── Dockerfile # Production build (nginx)
│ ├── Dockerfile.dev # Development build (Vite)
│ └── package.json
├── docker-compose.yml # Production compose file
├── docker-compose.dev.yml # Development compose file
├── docker-compose.prod.yml # Local production testing
├── API_DOCUMENTATION.md # Detailed API reference
└── README.md # This file
```
### Key Directories
- **`backend/constants/`** - Classification definitions (item types, groups, zones)
- **`backend/middleware/`** - Authentication, authorization, and image processing
- **`frontend/src/components/`** - Organized into 5 categories (common, forms, items, layout, modals)
- **`frontend/src/styles/`** - Theme system with CSS custom properties
- **`.gitea/workflows/`** - CI/CD pipeline for automated deployment
---
## 🔨 Development Workflow
### Component Organization
Frontend components are organized by feature:
```
components/
├── common/ # Reusable UI components
│ ├── ErrorMessage.jsx
│ ├── FloatingActionButton.jsx
│ ├── FormInput.jsx
│ ├── SortDropdown.jsx
│ └── UserRoleCard.jsx
├── forms/ # Form components
│ ├── AddItemForm.jsx
│ ├── ClassificationSection.jsx
│ └── ImageUploadSection.jsx
├── items/ # Item-related components
│ ├── GroceryItem.tsx
│ ├── GroceryListItem.jsx
│ └── SuggestionList.tsx
├── layout/ # Layout components
│ ├── AppLayout.jsx
│ └── Navbar.jsx
└── modals/ # Modal dialogs
├── AddImageModal.jsx
├── AddItemWithDetailsModal.jsx
├── ConfirmBuyModal.jsx
├── EditItemModal.jsx
├── ImageModal.jsx
├── ImageUploadModal.jsx
├── ItemClassificationModal.jsx
└── SimilarItemModal.jsx
```
Each subdirectory has an `index.js` barrel export for cleaner imports.
### Theme System
The application uses CSS custom properties for consistent theming:
```css
/* Theme variables defined in frontend/src/styles/theme.css */
:root {
/* Colors */
--color-primary: #0066cc;
--color-secondary: #6c757d;
--color-success: #28a745;
--color-danger: #dc3545;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
/* Typography */
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', ...;
--font-size-base: 1rem;
/* And many more... */
}
```
### Code Standards
- **Backend**: CommonJS modules, async/await for asynchronous operations
- **Frontend**: ES6 modules, functional components with hooks
- **TypeScript**: Used for type-safe components (gradually migrating)
- **Naming**: camelCase for functions/variables, PascalCase for components
- **File Structure**: Feature-based organization over type-based
### Testing
Currently, the project uses manual testing. Automated testing infrastructure is planned for future development.
---
## 🚢 Deployment
### CI/CD Pipeline
The application uses Gitea Actions for automated deployment:
```yaml
# .gitea/workflows/deploy.yml
Workflow:
1. Build Stage:
- Install dependencies
- Run tests (if present)
- Build Docker images
- Tag with :latest and :<commit-sha>
- Push to private registry
2. Deploy Stage:
- SSH to production server
- Upload docker-compose.yml
- Pull latest images
- Restart containers
- Prune old images
3. Notify Stage:
- Send deployment status via webhook
```
### Production Architecture
```
Production Server
├── Nginx Reverse Proxy (Port 80/443)
│ ├── /api → Backend Container (Port 5000)
│ └── /* → Frontend Container (Port 3000)
├── Docker Compose
│ ├── backend:latest (from registry)
│ └── frontend:latest (from registry)
└── PostgreSQL (External, not containerized)
```
### Environment Configuration
**Production (`docker-compose.yml`):**
- Pulls pre-built images from registry
- Uses external PostgreSQL database
- Environment configured via `backend.env` and `frontend.env`
- Automatic restart on failure
**Development (`docker-compose.dev.yml`):**
- Builds images locally
- Volume mounts for hot-reload
- Uses local `.env` files
- Nodemon for backend, Vite HMR for frontend
### Deployment Process
1. **Commit and push to `main` branch**
```bash
git add .
git commit -m "Your commit message"
git push origin main
```
2. **CI/CD automatically:**
- Runs tests
- Builds Docker images
- Pushes to registry
- Deploys to production
3. **Manual deployment (if needed):**
```bash
ssh user@production-server
cd /opt/costco-app
docker compose pull
docker compose up -d --remove-orphans
```
---
## 🗄️ Database Schema
### Entity Relationship Diagram
```mermaid
erDiagram
users ||--o{ grocery_list : creates
users ||--o{ grocery_history : adds
grocery_list ||--o{ grocery_history : tracks
grocery_list ||--o| item_classification : has
users {
int id PK
string username UK
string password
string name
string role
}
grocery_list {
int id PK
string item_name
int quantity
boolean bought
bytea item_image
string image_mime_type
int added_by FK
timestamp modified_on
}
item_classification {
int id PK,FK
string item_type
string item_group
string zone
float confidence
string source
}
grocery_history {
int id PK
int list_item_id FK
int quantity
int added_by FK
timestamp added_on
}
```
### Key Relationships
- **users → grocery_list**: One-to-many (creator relationship)
- **users → grocery_history**: One-to-many (contributor relationship)
- **grocery_list → grocery_history**: One-to-many (tracks all additions/modifications)
- **grocery_list → item_classification**: One-to-one (optional classification data)
---
## 🤝 Contributing
Contributions are welcome! Please follow these guidelines:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
### Development Guidelines
- Follow existing code style and structure
- Add comments for complex logic
- Update documentation for new features
- Test thoroughly before submitting PR
---
## 📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
---
## 👤 Author
**Nico Saya**
- Repository: [git.nicosaya.com/nalalangan/costco-grocery-list](https://git.nicosaya.com/nalalangan/costco-grocery-list)
---
## 🙏 Acknowledgments
- React team for the excellent framework
- Express.js community for robust server framework
- PostgreSQL for reliable data persistence
- Sharp for efficient image processing
- All contributors and users of this application
---
## 📞 Support
For issues, questions, or feature requests, please:
1. Check existing issues in the repository
2. Create a new issue with detailed description
3. Include steps to reproduce (for bugs)
4. Tag appropriately (bug, enhancement, question, etc.)
---
**Last Updated:** January 4, 2026

View File

@ -38,11 +38,9 @@ exports.getItemByName = async (itemName) => {
exports.addOrUpdateItem = async (itemName, quantity, userId, imageBuffer = null, mimeType = null) => { exports.addOrUpdateItem = async (itemName, quantity, userId, imageBuffer = null, mimeType = null) => {
const lowerItemName = itemName.toLowerCase();
const result = await pool.query( const result = await pool.query(
"SELECT id, bought FROM grocery_list WHERE item_name ILIKE $1", "SELECT id, bought FROM grocery_list WHERE item_name ILIKE $1",
[lowerItemName] [itemName]
); );
if (result.rowCount > 0) { if (result.rowCount > 0) {
@ -75,7 +73,7 @@ exports.addOrUpdateItem = async (itemName, quantity, userId, imageBuffer = null,
`INSERT INTO grocery_list `INSERT INTO grocery_list
(item_name, quantity, added_by, item_image, image_mime_type) (item_name, quantity, added_by, item_image, image_mime_type)
VALUES ($1, $2, $3, $4, $5) RETURNING id`, VALUES ($1, $2, $3, $4, $5) RETURNING id`,
[lowerItemName, quantity, userId, imageBuffer, mimeType] [itemName, quantity, userId, imageBuffer, mimeType]
); );
return insert.rows[0].id; return insert.rows[0].id;
} }

View File

@ -0,0 +1,34 @@
#!/bin/bash
# Move to common/
mv components/FloatingActionButton.jsx components/common/
mv components/SortDropdown.jsx components/common/
mv components/ErrorMessage.jsx components/common/
mv components/FormInput.jsx components/common/
mv components/UserRoleCard.jsx components/common/
# Move to modals/
mv components/AddItemWithDetailsModal.jsx components/modals/
mv components/EditItemModal.jsx components/modals/
mv components/SimilarItemModal.jsx components/modals/
mv components/ConfirmBuyModal.jsx components/modals/
mv components/ImageModal.jsx components/modals/
mv components/AddImageModal.jsx components/modals/
mv components/ImageUploadModal.jsx components/modals/
mv components/ItemClassificationModal.jsx components/modals/
# Move to forms/
mv components/AddItemForm.jsx components/forms/
mv components/ImageUploadSection.jsx components/forms/
mv components/ClassificationSection.jsx components/forms/
# Move to items/
mv components/GroceryListItem.jsx components/items/
mv components/GroceryItem.tsx components/items/
mv components/SuggestionList.tsx components/items/
# Move to layout/
mv components/AppLayout.jsx components/layout/
mv components/Navbar.jsx components/layout/
echo "Components moved successfully!"

View File

@ -0,0 +1,15 @@
#!/bin/bash
# Move page styles
mv styles/GroceryList.css styles/pages/
mv styles/Login.css styles/pages/
mv styles/Register.css styles/pages/
# Move component styles
mv styles/Navbar.css styles/components/
mv styles/AddItemWithDetailsModal.css styles/components/
mv styles/EditItemModal.css styles/components/
mv styles/ImageUploadSection.css styles/components/
mv styles/ClassificationSection.css styles/components/
echo "Styles moved successfully!"