Complete documentation suite: - DATABASE.md: Full schema, RLS policies, functions, queries - API.md: Supabase client API, Edge functions, realtime - DEVELOPMENT.md: Setup, workflow, conventions, testing - DEPLOYMENT.md: Docker Compose, Coolify, monitoring, backups Ready for development to begin.
679 lines
13 KiB
Markdown
679 lines
13 KiB
Markdown
# Pantry - Development Guide
|
|
|
|
**Version:** 1.0
|
|
**Last Updated:** 2026-02-08
|
|
|
|
---
|
|
|
|
## 🚀 Quick Start
|
|
|
|
### Prerequisites
|
|
|
|
- **Node.js** 20+ (or Bun 1.0+)
|
|
- **Docker** & Docker Compose
|
|
- **Git**
|
|
- **Code editor** (VS Code recommended)
|
|
|
|
### Clone & Setup
|
|
|
|
```bash
|
|
# Clone repository
|
|
git clone https://gitea.jeanlucmakiola.de/pantry-app/pantry.git
|
|
cd pantry
|
|
|
|
# Install dependencies (using Bun)
|
|
bun install
|
|
|
|
# Copy environment template
|
|
cp .env.example .env
|
|
|
|
# Start Supabase (PostgreSQL + Auth + Realtime)
|
|
docker-compose -f docker/docker-compose.dev.yml up -d
|
|
|
|
# Run database migrations
|
|
cd supabase
|
|
supabase db reset # Creates schema + seeds data
|
|
|
|
# Start Nuxt dev server
|
|
cd ../app
|
|
bun run dev
|
|
|
|
# Access app at http://localhost:3000
|
|
```
|
|
|
|
---
|
|
|
|
## 📁 Project Structure
|
|
|
|
```
|
|
pantry/
|
|
├── app/ # Nuxt 4 frontend
|
|
│ ├── components/
|
|
│ │ ├── inventory/ # Inventory UI components
|
|
│ │ ├── scan/ # Barcode scanner components
|
|
│ │ ├── tags/ # Tag management
|
|
│ │ └── common/ # Shared UI components
|
|
│ ├── composables/ # Shared logic (Vue Composition API)
|
|
│ │ ├── useBarcode.ts
|
|
│ │ ├── useInventory.ts
|
|
│ │ └── useSupabase.ts
|
|
│ ├── pages/ # Route pages
|
|
│ │ ├── index.vue # Inventory list
|
|
│ │ ├── scan.vue # Barcode scanner
|
|
│ │ └── settings.vue # Settings
|
|
│ ├── utils/ # Pure functions
|
|
│ │ ├── conversions.ts # Unit conversion math
|
|
│ │ └── validation.ts
|
|
│ ├── nuxt.config.ts # Nuxt configuration
|
|
│ └── package.json
|
|
├── supabase/ # Database & backend
|
|
│ ├── migrations/ # SQL migrations
|
|
│ │ ├── 001_schema.sql
|
|
│ │ ├── 002_seed_units.sql
|
|
│ │ └── 003_rls.sql
|
|
│ ├── functions/ # Edge functions
|
|
│ │ └── product-lookup/
|
|
│ ├── seed/ # Seed data (JSON)
|
|
│ │ ├── units.json
|
|
│ │ └── tags.json
|
|
│ └── config.toml # Supabase config
|
|
├── docker/ # Docker configs
|
|
│ ├── docker-compose.dev.yml # Development
|
|
│ └── docker-compose.prod.yml # Production
|
|
├── docs/ # Documentation
|
|
├── scripts/ # Utility scripts
|
|
│ ├── seed-db.ts
|
|
│ └── export-schema.ts
|
|
├── .env.example
|
|
├── .gitignore
|
|
└── README.md
|
|
```
|
|
|
|
---
|
|
|
|
## 🛠️ Development Workflow
|
|
|
|
### 1. Create a Feature Branch
|
|
|
|
```bash
|
|
git checkout -b feature/barcode-scanner
|
|
```
|
|
|
|
### 2. Make Changes
|
|
|
|
**Add a new component:**
|
|
```bash
|
|
# Create component file
|
|
touch app/components/scan/BarcodeScanner.vue
|
|
|
|
# Use in a page
|
|
# app/pages/scan.vue
|
|
<template>
|
|
<BarcodeScanner @scan="handleScan" />
|
|
</template>
|
|
```
|
|
|
|
**Add a database migration:**
|
|
```bash
|
|
cd supabase
|
|
supabase migration new add_location_field
|
|
|
|
# Edit supabase/migrations/XXX_add_location_field.sql
|
|
ALTER TABLE inventory_items ADD COLUMN location TEXT;
|
|
|
|
# Apply locally
|
|
supabase db reset
|
|
```
|
|
|
|
### 3. Test Locally
|
|
|
|
```bash
|
|
# Run type check
|
|
bun run typecheck
|
|
|
|
# Run linter
|
|
bun run lint
|
|
|
|
# Run tests (when implemented)
|
|
bun run test
|
|
|
|
# E2E tests
|
|
bun run test:e2e
|
|
```
|
|
|
|
### 4. Commit & Push
|
|
|
|
```bash
|
|
git add .
|
|
git commit -m "feat: Add barcode scanner component"
|
|
git push origin feature/barcode-scanner
|
|
```
|
|
|
|
### 5. Create Pull Request
|
|
|
|
- Go to Gitea: https://gitea.jeanlucmakiola.de/pantry-app/pantry
|
|
- Create PR from your branch to `main`
|
|
- Request review
|
|
- Merge when approved
|
|
|
|
---
|
|
|
|
## 📝 Code Conventions
|
|
|
|
### Naming
|
|
|
|
**Files:**
|
|
- Components: `PascalCase.vue` (e.g., `BarcodeScanner.vue`)
|
|
- Composables: `camelCase.ts` (e.g., `useBarcode.ts`)
|
|
- Utils: `camelCase.ts` (e.g., `conversions.ts`)
|
|
- Pages: `kebab-case.vue` (e.g., `item-detail.vue`)
|
|
|
|
**Variables:**
|
|
- `camelCase` for variables, functions
|
|
- `PascalCase` for types, interfaces
|
|
- `SCREAMING_SNAKE_CASE` for constants
|
|
|
|
```typescript
|
|
// Good
|
|
const itemCount = 5
|
|
const fetchProducts = () => {}
|
|
interface Product { ... }
|
|
const MAX_ITEMS = 100
|
|
|
|
// Bad
|
|
const ItemCount = 5
|
|
const fetch_products = () => {}
|
|
interface product { ... }
|
|
const maxItems = 100 // for constants
|
|
```
|
|
|
|
### Vue Component Structure
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// 1. Imports
|
|
import { ref, computed } from 'vue'
|
|
import type { InventoryItem } from '~/types'
|
|
|
|
// 2. Props & Emits
|
|
interface Props {
|
|
item: InventoryItem
|
|
editable?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
editable: false
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
save: [item: InventoryItem]
|
|
cancel: []
|
|
}>()
|
|
|
|
// 3. Composables
|
|
const { convert } = useUnits()
|
|
const supabase = useSupabaseClient()
|
|
|
|
// 4. State
|
|
const quantity = ref(props.item.quantity)
|
|
const isEditing = ref(false)
|
|
|
|
// 5. Computed
|
|
const displayQuantity = computed(() =>
|
|
`${quantity.value} ${props.item.unit.abbreviation}`
|
|
)
|
|
|
|
// 6. Methods
|
|
const handleSave = async () => {
|
|
const { error } = await supabase
|
|
.from('inventory_items')
|
|
.update({ quantity: quantity.value })
|
|
.eq('id', props.item.id)
|
|
|
|
if (!error) emit('save', { ...props.item, quantity: quantity.value })
|
|
}
|
|
|
|
// 7. Lifecycle (if needed)
|
|
onMounted(() => {
|
|
console.log('Component mounted')
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="item-card">
|
|
<h3>{{ item.name }}</h3>
|
|
<p>{{ displayQuantity }}</p>
|
|
|
|
<button v-if="editable" @click="handleSave">
|
|
Save
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.item-card {
|
|
/* Prefer Tailwind classes in template */
|
|
/* Use scoped styles only for complex/unique styles */
|
|
}
|
|
</style>
|
|
```
|
|
|
|
### TypeScript
|
|
|
|
**Prefer interfaces over types:**
|
|
```typescript
|
|
// Good
|
|
interface InventoryItem {
|
|
id: string
|
|
name: string
|
|
quantity: number
|
|
}
|
|
|
|
// Only use type for unions, intersections
|
|
type Status = 'active' | 'expired'
|
|
```
|
|
|
|
**Use strict typing:**
|
|
```typescript
|
|
// Good
|
|
const fetchItem = async (id: string): Promise<InventoryItem | null> => {
|
|
const { data } = await supabase
|
|
.from('inventory_items')
|
|
.select('*')
|
|
.eq('id', id)
|
|
.single()
|
|
|
|
return data
|
|
}
|
|
|
|
// Bad (implicit any)
|
|
const fetchItem = async (id) => {
|
|
const { data } = await supabase...
|
|
return data
|
|
}
|
|
```
|
|
|
|
### Composables
|
|
|
|
**Naming:** Always start with `use`
|
|
|
|
**Structure:**
|
|
```typescript
|
|
// composables/useInventory.ts
|
|
export function useInventory() {
|
|
const supabase = useSupabaseClient<Database>()
|
|
const items = ref<InventoryItem[]>([])
|
|
const loading = ref(false)
|
|
|
|
const fetchItems = async () => {
|
|
loading.value = true
|
|
const { data, error } = await supabase
|
|
.from('inventory_items')
|
|
.select('*')
|
|
|
|
if (data) items.value = data
|
|
loading.value = false
|
|
}
|
|
|
|
const addItem = async (item: NewInventoryItem) => {
|
|
const { data, error } = await supabase
|
|
.from('inventory_items')
|
|
.insert(item)
|
|
.select()
|
|
.single()
|
|
|
|
if (data) items.value.push(data)
|
|
return { data, error }
|
|
}
|
|
|
|
// Return reactive state + methods
|
|
return {
|
|
items: readonly(items),
|
|
loading: readonly(loading),
|
|
fetchItems,
|
|
addItem
|
|
}
|
|
}
|
|
```
|
|
|
|
### Database Migrations
|
|
|
|
**Naming:**
|
|
```
|
|
001_initial_schema.sql
|
|
002_seed_defaults.sql
|
|
003_add_location_field.sql
|
|
```
|
|
|
|
**Structure:**
|
|
```sql
|
|
-- Migration: Add location field to inventory items
|
|
-- Created: 2026-02-08
|
|
|
|
BEGIN;
|
|
|
|
ALTER TABLE inventory_items
|
|
ADD COLUMN location TEXT;
|
|
|
|
-- Update existing items (optional)
|
|
UPDATE inventory_items
|
|
SET location = 'Pantry'
|
|
WHERE location IS NULL;
|
|
|
|
COMMIT;
|
|
```
|
|
|
|
**Rollback (optional):**
|
|
```sql
|
|
-- To rollback:
|
|
-- ALTER TABLE inventory_items DROP COLUMN location;
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 Testing
|
|
|
|
### Unit Tests (Vitest)
|
|
|
|
```typescript
|
|
// app/utils/conversions.test.ts
|
|
import { describe, it, expect } from 'vitest'
|
|
import { convertUnit } from './conversions'
|
|
|
|
describe('convertUnit', () => {
|
|
it('converts grams to kilograms', () => {
|
|
const result = convertUnit(500, {
|
|
conversion_factor: 0.001,
|
|
base_unit_id: 'kg'
|
|
}, {
|
|
conversion_factor: 1,
|
|
base_unit_id: null
|
|
})
|
|
|
|
expect(result).toBe(0.5)
|
|
})
|
|
|
|
it('throws error for incompatible units', () => {
|
|
expect(() => {
|
|
convertUnit(1,
|
|
{ unit_type: 'weight', conversion_factor: 1 },
|
|
{ unit_type: 'volume', conversion_factor: 1 }
|
|
)
|
|
}).toThrow('Cannot convert')
|
|
})
|
|
})
|
|
```
|
|
|
|
**Run tests:**
|
|
```bash
|
|
bun test
|
|
bun test --watch # Watch mode
|
|
```
|
|
|
|
### E2E Tests (Playwright)
|
|
|
|
```typescript
|
|
// tests/e2e/inventory.spec.ts
|
|
import { test, expect } from '@playwright/test'
|
|
|
|
test('can add item to inventory', async ({ page }) => {
|
|
await page.goto('/')
|
|
|
|
// Login
|
|
await page.fill('[name=email]', 'test@example.com')
|
|
await page.fill('[name=password]', 'password')
|
|
await page.click('button[type=submit]')
|
|
|
|
// Add item
|
|
await page.click('[data-testid=add-item]')
|
|
await page.fill('[name=name]', 'Test Item')
|
|
await page.fill('[name=quantity]', '2')
|
|
await page.click('button:has-text("Add")')
|
|
|
|
// Verify
|
|
await expect(page.locator('text=Test Item')).toBeVisible()
|
|
})
|
|
```
|
|
|
|
**Run E2E:**
|
|
```bash
|
|
bun run test:e2e
|
|
bun run test:e2e --ui # Interactive mode
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 Supabase Local Development
|
|
|
|
### Start Supabase
|
|
|
|
```bash
|
|
cd supabase
|
|
supabase start
|
|
```
|
|
|
|
**Outputs:**
|
|
```
|
|
API URL: http://localhost:54321
|
|
Studio URL: http://localhost:54323
|
|
Anon key: eyJhb...
|
|
Service key: eyJhb...
|
|
```
|
|
|
|
**Access Supabase Studio:**
|
|
- Open http://localhost:54323
|
|
- View tables, run queries, manage auth
|
|
|
|
### Apply Migrations
|
|
|
|
```bash
|
|
# Reset DB (drops all data, reapplies migrations)
|
|
supabase db reset
|
|
|
|
# Create new migration
|
|
supabase migration new add_feature
|
|
|
|
# Apply migrations (non-destructive)
|
|
supabase migration up
|
|
```
|
|
|
|
### Generate Types
|
|
|
|
```bash
|
|
# Generate TypeScript types from database schema
|
|
supabase gen types typescript --local > app/types/supabase.ts
|
|
```
|
|
|
|
**Usage:**
|
|
```typescript
|
|
import type { Database } from '~/types/supabase'
|
|
|
|
const supabase = useSupabaseClient<Database>()
|
|
```
|
|
|
|
---
|
|
|
|
## 🌐 Environment Variables
|
|
|
|
### `.env` (Development)
|
|
|
|
```bash
|
|
# Supabase (from `supabase start`)
|
|
SUPABASE_URL=http://localhost:54321
|
|
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
|
|
# App
|
|
PUBLIC_APP_URL=http://localhost:3000
|
|
|
|
# Open Food Facts (optional, no key needed)
|
|
OPENFOODFACTS_API_URL=https://world.openfoodfacts.org
|
|
```
|
|
|
|
### `.env.production`
|
|
|
|
```bash
|
|
# Supabase (production)
|
|
SUPABASE_URL=https://your-project.supabase.co
|
|
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
|
|
# App
|
|
PUBLIC_APP_URL=https://pantry.yourdomain.com
|
|
```
|
|
|
|
---
|
|
|
|
## 🐛 Debugging
|
|
|
|
### Nuxt DevTools
|
|
|
|
**Enable:**
|
|
```typescript
|
|
// nuxt.config.ts
|
|
export default defineNuxtConfig({
|
|
devtools: { enabled: true }
|
|
})
|
|
```
|
|
|
|
**Access:** Press `Shift + Alt + D` or visit http://localhost:3000/__devtools__
|
|
|
|
### Vue DevTools
|
|
|
|
Install browser extension:
|
|
- Chrome: [Vue DevTools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
|
- Firefox: [Vue DevTools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
|
|
|
### Supabase Logs
|
|
|
|
```bash
|
|
# View realtime logs
|
|
supabase logs --tail
|
|
|
|
# Filter by service
|
|
supabase logs --service postgres
|
|
supabase logs --service auth
|
|
```
|
|
|
|
### Database Queries
|
|
|
|
**Supabase Studio:**
|
|
- Open http://localhost:54323
|
|
- Go to "SQL Editor"
|
|
- Run queries directly
|
|
|
|
**CLI:**
|
|
```bash
|
|
# psql into local database
|
|
supabase db shell
|
|
|
|
# Run query
|
|
SELECT * FROM inventory_items LIMIT 5;
|
|
```
|
|
|
|
---
|
|
|
|
## 📦 Build & Preview
|
|
|
|
### Build for Production
|
|
|
|
```bash
|
|
cd app
|
|
bun run build
|
|
```
|
|
|
|
**Output:** `.output/` directory (ready for deployment)
|
|
|
|
### Preview Production Build
|
|
|
|
```bash
|
|
bun run preview
|
|
```
|
|
|
|
**Access:** http://localhost:3000
|
|
|
|
---
|
|
|
|
## 🔄 Git Workflow
|
|
|
|
### Branch Naming
|
|
|
|
```
|
|
feature/barcode-scanner
|
|
fix/tag-duplication-bug
|
|
chore/update-dependencies
|
|
docs/api-reference
|
|
```
|
|
|
|
### Commit Messages (Conventional Commits)
|
|
|
|
```bash
|
|
# Format: <type>(<scope>): <subject>
|
|
|
|
feat(scan): add barcode detection
|
|
fix(inventory): prevent duplicate items
|
|
chore(deps): update Nuxt to 4.1
|
|
docs(api): add product lookup endpoint
|
|
refactor(tags): simplify tag picker logic
|
|
test(units): add conversion edge cases
|
|
style(ui): apply Tailwind spacing
|
|
```
|
|
|
|
**Types:**
|
|
- `feat`: New feature
|
|
- `fix`: Bug fix
|
|
- `chore`: Maintenance (deps, config)
|
|
- `docs`: Documentation
|
|
- `refactor`: Code restructuring
|
|
- `test`: Adding/updating tests
|
|
- `style`: Code style (formatting, no logic change)
|
|
|
|
---
|
|
|
|
## 🚨 Common Issues
|
|
|
|
### Supabase won't start
|
|
|
|
```bash
|
|
# Check Docker
|
|
docker ps
|
|
|
|
# Restart Supabase
|
|
supabase stop
|
|
supabase start
|
|
```
|
|
|
|
### Types out of sync
|
|
|
|
```bash
|
|
# Regenerate types after schema change
|
|
supabase gen types typescript --local > app/types/supabase.ts
|
|
```
|
|
|
|
### Port already in use
|
|
|
|
```bash
|
|
# Nuxt (3000)
|
|
lsof -ti:3000 | xargs kill
|
|
|
|
# Supabase (54321)
|
|
supabase stop
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Resources
|
|
|
|
**Docs:**
|
|
- [Nuxt 4](https://nuxt.com)
|
|
- [Supabase](https://supabase.com/docs)
|
|
- [Vue 3](https://vuejs.org)
|
|
- [Tailwind CSS](https://tailwindcss.com)
|
|
|
|
**Community:**
|
|
- [Pantry Discussions](https://gitea.jeanlucmakiola.de/pantry-app/pantry/issues)
|
|
- [Nuxt Discord](https://discord.com/invite/ps2h6QT)
|
|
- [Supabase Discord](https://discord.supabase.com)
|
|
|
|
---
|
|
|
|
**Next:** [Deployment Guide](./DEPLOYMENT.md)
|