Compare commits
9 Commits
5805be698b
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d35a3e7b3 | ||
| 670b2f9200 | |||
|
|
521e3f552f | ||
| 627e970986 | |||
|
|
50a0bd9417 | ||
| 097f0f9cee | |||
|
|
b1ef7e43be | ||
|
|
12bda4c08f | ||
|
|
5eb0d04377 |
64
.env.example
64
.env.example
@@ -1,54 +1,20 @@
|
||||
# Pantry - Environment Variables Template
|
||||
# Copy to .env.development for local development
|
||||
# Copy to .env.production for production deployment
|
||||
# Supabase Local Development Environment
|
||||
# Copy this file to .env and adjust as needed
|
||||
|
||||
# ==============================================
|
||||
# Supabase Configuration
|
||||
# ==============================================
|
||||
# PostgreSQL
|
||||
POSTGRES_PASSWORD=postgres
|
||||
|
||||
# Supabase API URL (from Coolify service)
|
||||
SUPABASE_URL=https://your-supabase-instance.example.com
|
||||
# JWT Secret (generate with: openssl rand -base64 32)
|
||||
# Default is fine for local dev, change for production
|
||||
JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
|
||||
|
||||
# Supabase Anon Key (public, safe to expose to frontend)
|
||||
SUPABASE_ANON_KEY=your-anon-key-here
|
||||
# API Keys
|
||||
# These are Supabase's default demo keys - OK for local development
|
||||
# For production, generate new keys: https://supabase.com/docs/guides/self-hosting#api-keys
|
||||
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
|
||||
|
||||
# Supabase Service Role Key (SECRET - server-side only, never expose to frontend)
|
||||
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
|
||||
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
|
||||
|
||||
# JWT Secret (for Supabase Auth)
|
||||
SUPABASE_JWT_SECRET=your-jwt-secret-here
|
||||
|
||||
# Database Password (for direct PostgreSQL access if needed)
|
||||
POSTGRES_PASSWORD=your-postgres-password-here
|
||||
|
||||
# ==============================================
|
||||
# Application Configuration
|
||||
# ==============================================
|
||||
|
||||
# Public app URL (where the Nuxt app is hosted)
|
||||
PUBLIC_APP_URL=http://localhost:3000
|
||||
|
||||
# Node environment
|
||||
NODE_ENV=development
|
||||
|
||||
# ==============================================
|
||||
# External APIs
|
||||
# ==============================================
|
||||
|
||||
# Open Food Facts API (no auth required)
|
||||
OPENFOODFACTS_API_URL=https://world.openfoodfacts.org
|
||||
|
||||
# ==============================================
|
||||
# Optional: Auth Providers (OIDC)
|
||||
# ==============================================
|
||||
|
||||
# Google OAuth (optional - configure in Supabase if needed)
|
||||
AUTH_GOOGLE_ENABLED=false
|
||||
AUTH_GOOGLE_CLIENT_ID=
|
||||
AUTH_GOOGLE_SECRET=
|
||||
|
||||
# Authentik (optional)
|
||||
AUTH_AUTHENTIK_ENABLED=false
|
||||
AUTH_AUTHENTIK_URL=
|
||||
AUTH_AUTHENTIK_CLIENT_ID=
|
||||
AUTH_AUTHENTIK_SECRET=
|
||||
# Nuxt App Configuration (also copy to app/.env)
|
||||
NUXT_PUBLIC_SUPABASE_URL=http://localhost:54321
|
||||
NUXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
|
||||
|
||||
54
.env.example.bak
Normal file
54
.env.example.bak
Normal file
@@ -0,0 +1,54 @@
|
||||
# Pantry - Environment Variables Template
|
||||
# Copy to .env.development for local development
|
||||
# Copy to .env.production for production deployment
|
||||
|
||||
# ==============================================
|
||||
# Supabase Configuration
|
||||
# ==============================================
|
||||
|
||||
# Supabase API URL (from Coolify service)
|
||||
SUPABASE_URL=https://your-supabase-instance.example.com
|
||||
|
||||
# Supabase Anon Key (public, safe to expose to frontend)
|
||||
SUPABASE_ANON_KEY=your-anon-key-here
|
||||
|
||||
# Supabase Service Role Key (SECRET - server-side only, never expose to frontend)
|
||||
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
|
||||
|
||||
# JWT Secret (for Supabase Auth)
|
||||
SUPABASE_JWT_SECRET=your-jwt-secret-here
|
||||
|
||||
# Database Password (for direct PostgreSQL access if needed)
|
||||
POSTGRES_PASSWORD=your-postgres-password-here
|
||||
|
||||
# ==============================================
|
||||
# Application Configuration
|
||||
# ==============================================
|
||||
|
||||
# Public app URL (where the Nuxt app is hosted)
|
||||
PUBLIC_APP_URL=http://localhost:3000
|
||||
|
||||
# Node environment
|
||||
NODE_ENV=development
|
||||
|
||||
# ==============================================
|
||||
# External APIs
|
||||
# ==============================================
|
||||
|
||||
# Open Food Facts API (no auth required)
|
||||
OPENFOODFACTS_API_URL=https://world.openfoodfacts.org
|
||||
|
||||
# ==============================================
|
||||
# Optional: Auth Providers (OIDC)
|
||||
# ==============================================
|
||||
|
||||
# Google OAuth (optional - configure in Supabase if needed)
|
||||
AUTH_GOOGLE_ENABLED=false
|
||||
AUTH_GOOGLE_CLIENT_ID=
|
||||
AUTH_GOOGLE_SECRET=
|
||||
|
||||
# Authentik (optional)
|
||||
AUTH_AUTHENTIK_ENABLED=false
|
||||
AUTH_AUTHENTIK_URL=
|
||||
AUTH_AUTHENTIK_CLIENT_ID=
|
||||
AUTH_AUTHENTIK_SECRET=
|
||||
65
README.md
65
README.md
@@ -22,26 +22,37 @@ A simple, modern kitchen inventory app that the whole family can actually use. B
|
||||
- 🔒 **Self-hosted** — Your data stays yours
|
||||
- 🌐 **Open Food Facts** — Auto-fill product data from barcodes
|
||||
|
||||
## 🚀 Quick Start
|
||||
## 🚀 Quick Start (Local Development)
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
# Clone repository
|
||||
git clone https://gitea.jeanlucmakiola.de/pantry-app/pantry.git
|
||||
cd pantry
|
||||
|
||||
# Start services (Docker Compose)
|
||||
docker-compose up -d
|
||||
|
||||
# Access at http://localhost:3000
|
||||
# One-command startup
|
||||
./dev.sh
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
1. Starts Supabase (PostgreSQL + API + Auth + Studio)
|
||||
2. Installs frontend dependencies
|
||||
3. Launches Nuxt dev server
|
||||
|
||||
**Access:**
|
||||
- App: `http://localhost:3000`
|
||||
- Supabase Studio: `http://localhost:54323`
|
||||
|
||||
**See [DEV_SETUP.md](DEV_SETUP.md) for detailed setup guide.**
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- [**Project Plan**](docs/PROJECT_PLAN.md) — Vision, roadmap, phases
|
||||
- [**Architecture**](docs/ARCHITECTURE.md) — Tech stack, data model, design decisions
|
||||
- [**Database Schema**](docs/DATABASE.md) — Tables, relationships, RLS policies
|
||||
- [**API Reference**](docs/API.md) — Endpoints, Supabase functions
|
||||
- [**Development Guide**](docs/DEVELOPMENT.md) — Setup, workflow, conventions
|
||||
- [**Deployment**](docs/DEPLOYMENT.md) — Docker, Coolify, production setup
|
||||
- **[Getting Started](docs/development/getting-started.md)** — First-time setup (5 minutes)
|
||||
- **[Local Setup Guide](docs/development/local-setup.md)** — Detailed Docker Compose setup
|
||||
- **[Project Plan](docs/PROJECT_PLAN.md)** — Vision, roadmap, MVP phases
|
||||
- **[Architecture](docs/architecture/overview.md)** — Tech stack, design decisions
|
||||
- **[Database Schema](docs/architecture/database.md)** — Tables, RLS policies, migrations
|
||||
- **[Development Workflow](docs/development/workflow.md)** — Git flow, conventions
|
||||
- **[Full Documentation Index](docs/README.md)** — Complete docs navigation
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
@@ -73,18 +84,32 @@ pantry/
|
||||
3. **Extendable** — Clean architecture for future features
|
||||
4. **Self-hosted first** — No SaaS plans, no lock-in
|
||||
|
||||
## 📋 MVP Status
|
||||
## 📋 MVP Status (14/34 Complete - 41.2%)
|
||||
|
||||
**Target:** v0.1 (6-week sprint)
|
||||
**Current Phase:** Week 2 ✅ Complete, Week 3 🔄 In Progress
|
||||
|
||||
✅ **Week 1 - Foundation (6/6)**
|
||||
- Database schema + RLS policies
|
||||
- Nuxt 4 app scaffold
|
||||
- Supabase integration
|
||||
- App layout
|
||||
|
||||
✅ **Week 2 - Core Inventory (8/8)**
|
||||
- SQL helper functions
|
||||
- Seed data (units + tags)
|
||||
- Inventory CRUD UI
|
||||
- Add/Edit/Delete components
|
||||
|
||||
🔄 **Week 3 - Barcode Scanning (1/5)**
|
||||
- BarcodeScanner component
|
||||
- html5-qrcode integration
|
||||
- Product lookup (pending)
|
||||
- Scan-to-add flow (pending)
|
||||
|
||||
⏸️ **Week 4-6** - Tag UI, PWA, Deployment (20 issues)
|
||||
|
||||
See [PROJECT_PLAN.md](docs/PROJECT_PLAN.md) for detailed roadmap.
|
||||
|
||||
- [ ] Foundation (Nuxt + Supabase + Auth)
|
||||
- [ ] Core inventory (CRUD, tags, units)
|
||||
- [ ] Barcode scanning (PWA camera + Open Food Facts)
|
||||
- [ ] Mobile polish (PWA, offline)
|
||||
- [ ] Docker deployment
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
This is an early-stage project. Contributions welcome once v0.1 ships.
|
||||
|
||||
214
SETUP.md
214
SETUP.md
@@ -1,214 +0,0 @@
|
||||
# Pantry - Setup Guide
|
||||
|
||||
## ✅ Current Status
|
||||
|
||||
### Infrastructure (Complete)
|
||||
|
||||
- **Supabase Dev Instance**: Running on Coolify
|
||||
- URL: `https://supabasekong-ewo8wssk4gs8cgg0c8kosk40.jeanlucmakiola.de`
|
||||
- Status: ✅ Healthy
|
||||
- Services: PostgreSQL, Auth (GoTrue), Realtime, Storage, PostgREST
|
||||
|
||||
### Environment Configuration (Complete)
|
||||
|
||||
- `.env.example` - Template for all environments
|
||||
- `.env.development` - Dev credentials (Coolify Supabase)
|
||||
- `.gitignore` - Protects secrets
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start (Development)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Bun** 1.0+ (or Node.js 20+)
|
||||
- **Git** access to repository
|
||||
- **Access to Coolify** Supabase instance (credentials in `.env.development`)
|
||||
|
||||
### Setup Steps
|
||||
|
||||
1. **Clone Repository**
|
||||
```bash
|
||||
git clone https://gitea.jeanlucmakiola.de/pantry-app/pantry.git
|
||||
cd pantry
|
||||
```
|
||||
|
||||
2. **Copy Development Environment**
|
||||
```bash
|
||||
cp .env.development .env
|
||||
```
|
||||
|
||||
3. **Install Dependencies** (once `app/` exists)
|
||||
```bash
|
||||
cd app
|
||||
bun install
|
||||
```
|
||||
|
||||
4. **Apply Database Migrations** (once created)
|
||||
```bash
|
||||
cd supabase
|
||||
# TBD: Migration command
|
||||
```
|
||||
|
||||
5. **Start Development Server**
|
||||
```bash
|
||||
cd app
|
||||
bun run dev
|
||||
```
|
||||
|
||||
6. **Access App**
|
||||
- App: `http://localhost:3000`
|
||||
- Supabase API: `https://supabasekong-ewo8wssk4gs8cgg0c8kosk40.jeanlucmakiola.de`
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Supabase Instance Details
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Service | URL |
|
||||
|---------|-----|
|
||||
| API (PostgREST) | `https://supabasekong-ewo8wssk4gs8cgg0c8kosk40.jeanlucmakiola.de/rest/v1/` |
|
||||
| Auth | `https://supabasekong-ewo8wssk4gs8cgg0c8kosk40.jeanlucmakiola.de/auth/v1/` |
|
||||
| Realtime | `wss://supabasekong-ewo8wssk4gs8cgg0c8kosk40.jeanlucmakiola.de/realtime/v1/` |
|
||||
| Storage | `https://supabasekong-ewo8wssk4gs8cgg0c8kosk40.jeanlucmakiola.de/storage/v1/` |
|
||||
|
||||
### Credentials
|
||||
|
||||
**Public (safe to use in frontend):**
|
||||
- Anon Key: In `.env.development`
|
||||
|
||||
**Secret (server-side only):**
|
||||
- Service Role Key: In `.env.development`
|
||||
- JWT Secret: In `.env.development`
|
||||
- Postgres Password: In `.env.development`
|
||||
|
||||
### Dashboard Access
|
||||
|
||||
**Supabase Studio:**
|
||||
- URL: `https://supabasekong-ewo8wssk4gs8cgg0c8kosk40.jeanlucmakiola.de` (check Coolify for Studio port/subdomain)
|
||||
- Username: `wJZbjs3Yd5P63cs9`
|
||||
- Password: `Qv3byDujNzYe8r7YRxhNwh3DPTvZBWtN`
|
||||
|
||||
**Direct PostgreSQL Access** (for migrations/debugging):
|
||||
- Host: `supabase-db` (or Coolify service FQDN)
|
||||
- Database: `postgres`
|
||||
- User: `postgres`
|
||||
- Password: `55P0NVRUltRqzZYksuXTFli5iXwbQvgu`
|
||||
- Port: `5432`
|
||||
|
||||
**MinIO (Storage):**
|
||||
- Admin User: `EaTXrXvjo1R4hsaI`
|
||||
- Admin Password: `gCZOphxAExNC17GYFwtw60WzTU0P8HW8`
|
||||
|
||||
---
|
||||
|
||||
## 📋 Next Steps
|
||||
|
||||
### Week 1: Foundation (In Progress)
|
||||
|
||||
- [x] ~~Supabase dev environment setup~~ (Complete)
|
||||
- [x] ~~Environment configuration~~ (Complete)
|
||||
- [ ] Create database schema (`supabase/migrations/`)
|
||||
- [ ] Scaffold Nuxt 4 app (`app/`)
|
||||
- [ ] Implement email/password auth
|
||||
- [ ] Deploy first version to Coolify
|
||||
|
||||
### Immediate Tasks
|
||||
|
||||
1. **Database Schema** (#10)
|
||||
- Create migration files in `supabase/migrations/`
|
||||
- Tables: `inventory_items`, `products`, `tags`, `item_tags`, `units`
|
||||
- See: `docs/DATABASE.md`
|
||||
|
||||
2. **Nuxt Scaffold** (#8)
|
||||
- Initialize Nuxt 4 project in `app/`
|
||||
- Install dependencies: `@nuxtjs/supabase`, `@nuxt/ui`, Tailwind
|
||||
- Configure `nuxt.config.ts`
|
||||
|
||||
3. **Auth Implementation** (#11)
|
||||
- Supabase Auth integration
|
||||
- Login/signup pages
|
||||
- Protected routes
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Development Workflow
|
||||
|
||||
### Making Changes
|
||||
|
||||
```bash
|
||||
# 1. Create feature branch
|
||||
git checkout -b feature/your-feature
|
||||
|
||||
# 2. Make changes
|
||||
# Edit files...
|
||||
|
||||
# 3. Test locally
|
||||
bun run dev
|
||||
|
||||
# 4. Commit and push
|
||||
git add .
|
||||
git commit -m "feat: Your feature description"
|
||||
git push origin feature/your-feature
|
||||
|
||||
# 5. Create PR on Gitea
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
|
||||
```bash
|
||||
# Create new migration
|
||||
cd supabase/migrations
|
||||
touch 001_initial_schema.sql
|
||||
|
||||
# Edit migration file (SQL)
|
||||
# Test locally against Coolify Supabase instance
|
||||
|
||||
# Apply migration (TBD - once we set up migration tooling)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Can't connect to Supabase
|
||||
|
||||
**Test connection:**
|
||||
```bash
|
||||
curl -s "https://supabasekong-ewo8wssk4gs8cgg0c8kosk40.jeanlucmakiola.de/rest/v1/" \
|
||||
-H "apikey: <ANON_KEY>"
|
||||
```
|
||||
|
||||
Should return OpenAPI spec. If not:
|
||||
- Check Coolify service status
|
||||
- Verify URL in `.env`
|
||||
- Check network/firewall
|
||||
|
||||
### Environment variables not loading
|
||||
|
||||
- Ensure `.env` exists in project root
|
||||
- Check `.env` has no syntax errors
|
||||
- Restart dev server after changes
|
||||
|
||||
### Database migration issues
|
||||
|
||||
- Verify `SUPABASE_SERVICE_ROLE_KEY` is set
|
||||
- Check migration SQL syntax
|
||||
- Review Supabase logs in Coolify
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- [Project Plan](docs/PROJECT_PLAN.md) - Vision, roadmap
|
||||
- [Architecture](docs/ARCHITECTURE.md) - Tech stack, design
|
||||
- [Database Schema](docs/DATABASE.md) - Tables, RLS, functions
|
||||
- [API Reference](docs/API.md) - Endpoints, usage
|
||||
- [Development Guide](docs/DEVELOPMENT.md) - Conventions, workflow
|
||||
- [Deployment](docs/DEPLOYMENT.md) - Docker, Coolify
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-02-08
|
||||
**Status:** Week 1 - Foundation in progress
|
||||
@@ -131,6 +131,16 @@ const { addInventoryItem, addItemTags } = useInventory()
|
||||
const { getUnits } = useUnits()
|
||||
const { getTags } = useTags()
|
||||
|
||||
const props = defineProps<{
|
||||
initialData?: {
|
||||
barcode?: string
|
||||
name?: string
|
||||
brand?: string
|
||||
image_url?: string
|
||||
quantity?: string
|
||||
}
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
added: [item: any]
|
||||
@@ -166,6 +176,40 @@ onMounted(async () => {
|
||||
if (defaultUnit) {
|
||||
form.unit_id = defaultUnit.id
|
||||
}
|
||||
|
||||
// Pre-fill from initial data (scan-to-add flow)
|
||||
if (props.initialData) {
|
||||
if (props.initialData.name) {
|
||||
form.name = props.initialData.name
|
||||
}
|
||||
|
||||
// Add brand to notes if available
|
||||
if (props.initialData.brand) {
|
||||
form.notes = `Brand: ${props.initialData.brand}`
|
||||
|
||||
if (props.initialData.barcode) {
|
||||
form.notes += `\nBarcode: ${props.initialData.barcode}`
|
||||
}
|
||||
} else if (props.initialData.barcode) {
|
||||
form.notes = `Barcode: ${props.initialData.barcode}`
|
||||
}
|
||||
|
||||
// Parse quantity if available (e.g., "750g")
|
||||
if (props.initialData.quantity) {
|
||||
const quantityMatch = props.initialData.quantity.match(/^([\d.]+)\s*([a-zA-Z]+)$/)
|
||||
if (quantityMatch) {
|
||||
form.quantity = parseFloat(quantityMatch[1])
|
||||
// Try to match unit
|
||||
const unitAbbr = quantityMatch[2].toLowerCase()
|
||||
const matchedUnit = units.value.find(u =>
|
||||
u.abbreviation.toLowerCase() === unitAbbr
|
||||
)
|
||||
if (matchedUnit) {
|
||||
form.unit_id = matchedUnit.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Unit options for select
|
||||
|
||||
61
app/composables/useProductLookup.ts
Normal file
61
app/composables/useProductLookup.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// Composable for product lookup via Edge Function
|
||||
|
||||
export interface ProductData {
|
||||
barcode: string
|
||||
name: string
|
||||
brand?: string
|
||||
quantity?: string
|
||||
image_url?: string
|
||||
category?: string
|
||||
cached?: boolean
|
||||
}
|
||||
|
||||
export const useProductLookup = () => {
|
||||
const supabase = useSupabaseClient()
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const lookupProduct = async (barcode: string): Promise<ProductData | null> => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const { data, error: functionError } = await supabase.functions.invoke('product-lookup', {
|
||||
body: { barcode }
|
||||
})
|
||||
|
||||
if (functionError) {
|
||||
console.error('Product lookup error:', functionError)
|
||||
error.value = functionError.message || 'Failed to lookup product'
|
||||
|
||||
// Return basic product data even on error
|
||||
return {
|
||||
barcode,
|
||||
name: `Product ${barcode}`,
|
||||
cached: false
|
||||
}
|
||||
}
|
||||
|
||||
return data as ProductData
|
||||
|
||||
} catch (err) {
|
||||
console.error('Unexpected error during product lookup:', err)
|
||||
error.value = err instanceof Error ? err.message : 'Unknown error'
|
||||
|
||||
// Return basic product data even on error
|
||||
return {
|
||||
barcode,
|
||||
name: `Product ${barcode}`,
|
||||
cached: false
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
lookupProduct,
|
||||
isLoading: readonly(isLoading),
|
||||
error: readonly(error)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,8 @@
|
||||
<div v-if="showAddForm" class="fixed inset-0 z-50 flex items-start justify-center pt-20 px-4 bg-black/50">
|
||||
<div class="w-full max-w-lg">
|
||||
<AddItemForm
|
||||
@close="showAddForm = false"
|
||||
:initial-data="prefilledData"
|
||||
@close="handleCloseAddForm"
|
||||
@added="handleItemAdded"
|
||||
/>
|
||||
</div>
|
||||
@@ -56,13 +57,42 @@ definePageMeta({
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const showAddForm = ref(false)
|
||||
const editingItem = ref<any>(null)
|
||||
const refreshKey = ref(0)
|
||||
const inventoryListRef = ref()
|
||||
const prefilledData = ref<any>(null)
|
||||
|
||||
// Handle scan-to-add flow (Issue #25)
|
||||
onMounted(() => {
|
||||
if (route.query.action === 'add') {
|
||||
// Pre-fill data from query params (from scan)
|
||||
prefilledData.value = {
|
||||
barcode: route.query.barcode as string || undefined,
|
||||
name: route.query.name as string || undefined,
|
||||
brand: route.query.brand as string || undefined,
|
||||
image_url: route.query.image_url as string || undefined,
|
||||
quantity: route.query.quantity as string || undefined,
|
||||
}
|
||||
|
||||
showAddForm.value = true
|
||||
|
||||
// Clean up URL
|
||||
router.replace({ query: {} })
|
||||
}
|
||||
})
|
||||
|
||||
const handleCloseAddForm = () => {
|
||||
showAddForm.value = false
|
||||
prefilledData.value = null
|
||||
}
|
||||
|
||||
const handleItemAdded = (item: any) => {
|
||||
showAddForm.value = false
|
||||
prefilledData.value = null
|
||||
// Reload the inventory list
|
||||
inventoryListRef.value?.reload()
|
||||
}
|
||||
|
||||
@@ -2,25 +2,64 @@
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-6">Scan Item</h1>
|
||||
|
||||
<UCard>
|
||||
<div class="text-center py-12">
|
||||
<UIcon
|
||||
name="i-heroicons-qr-code"
|
||||
class="w-16 h-16 text-gray-400 mx-auto mb-4"
|
||||
<UCard v-if="!scannedBarcode" class="mb-6">
|
||||
<ScanBarcodeScanner
|
||||
@barcode-detected="handleBarcodeDetected"
|
||||
@manual-entry="showManualEntry = true"
|
||||
/>
|
||||
</UCard>
|
||||
|
||||
<!-- Product Lookup Result -->
|
||||
<UCard v-if="productData" class="mb-6">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<img
|
||||
v-if="productData.image_url"
|
||||
:src="productData.image_url"
|
||||
:alt="productData.name"
|
||||
class="w-24 h-24 object-cover rounded"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-bold mb-1">{{ productData.name }}</h3>
|
||||
<p v-if="productData.brand" class="text-gray-600">{{ productData.brand }}</p>
|
||||
<p class="text-sm text-gray-500 mt-2">Barcode: {{ scannedBarcode }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="lookupError"
|
||||
color="yellow"
|
||||
icon="i-heroicons-exclamation-triangle"
|
||||
title="Product not found"
|
||||
:description="lookupError"
|
||||
/>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
||||
Barcode Scanner
|
||||
</h3>
|
||||
<p class="text-gray-600 mb-6">
|
||||
This feature will be implemented in Week 3.
|
||||
</p>
|
||||
<UButton
|
||||
to="/"
|
||||
color="gray"
|
||||
variant="soft"
|
||||
>
|
||||
Back to Inventory
|
||||
</UButton>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<UButton
|
||||
color="primary"
|
||||
size="lg"
|
||||
icon="i-heroicons-plus"
|
||||
class="flex-1"
|
||||
@click="addToInventory"
|
||||
>
|
||||
Add to Inventory
|
||||
</UButton>
|
||||
<UButton
|
||||
color="gray"
|
||||
size="lg"
|
||||
@click="resetScanner"
|
||||
>
|
||||
Scan Again
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Loading State -->
|
||||
<UCard v-if="isLookingUp">
|
||||
<div class="text-center py-8">
|
||||
<div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mb-4"></div>
|
||||
<p class="text-gray-600">Looking up product...</p>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
@@ -30,4 +69,44 @@
|
||||
definePageMeta({
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
const scannedBarcode = ref<string | null>(null)
|
||||
const productData = ref<any>(null)
|
||||
const showManualEntry = ref(false)
|
||||
|
||||
// Use product lookup composable
|
||||
const { lookupProduct, isLoading: isLookingUp, error: lookupError } = useProductLookup()
|
||||
|
||||
const handleBarcodeDetected = async (barcode: string) => {
|
||||
scannedBarcode.value = barcode
|
||||
|
||||
// Fetch product data from Edge Function
|
||||
const data = await lookupProduct(barcode)
|
||||
|
||||
if (data) {
|
||||
productData.value = data
|
||||
}
|
||||
}
|
||||
|
||||
const addToInventory = () => {
|
||||
// Navigate to home page with add form open and pre-filled
|
||||
navigateTo({
|
||||
path: '/',
|
||||
query: {
|
||||
action: 'add',
|
||||
barcode: scannedBarcode.value,
|
||||
name: productData.value?.name || undefined,
|
||||
brand: productData.value?.brand || undefined,
|
||||
image_url: productData.value?.image_url || undefined,
|
||||
quantity: productData.value?.quantity || undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetScanner = () => {
|
||||
scannedBarcode.value = null
|
||||
productData.value = null
|
||||
lookupError.value = null
|
||||
isLookingUp.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
42
dev.sh
Executable file
42
dev.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 Pantry - Starting Local Development Environment"
|
||||
echo ""
|
||||
|
||||
# Check prerequisites
|
||||
command -v docker >/dev/null 2>&1 || { echo "❌ Docker not found. Install: https://docs.docker.com/get-docker/"; exit 1; }
|
||||
command -v docker-compose >/dev/null 2>&1 || { echo "❌ Docker Compose not found. Install: https://docs.docker.com/compose/install/"; exit 1; }
|
||||
command -v bun >/dev/null 2>&1 || { echo "❌ Bun not found. Install: curl -fsSL https://bun.sh/install | bash"; exit 1; }
|
||||
|
||||
echo "✅ Prerequisites OK"
|
||||
echo ""
|
||||
|
||||
# Step 1: Start Supabase
|
||||
echo "📦 Starting Supabase services..."
|
||||
docker-compose up -d
|
||||
|
||||
echo "⏳ Waiting for services to initialize (15s)..."
|
||||
sleep 15
|
||||
|
||||
echo "✅ Supabase started:"
|
||||
echo " - API: http://localhost:54321"
|
||||
echo " - Studio: http://localhost:54323"
|
||||
echo ""
|
||||
|
||||
# Step 2: Install frontend dependencies
|
||||
echo "📦 Installing frontend dependencies..."
|
||||
cd app
|
||||
bun install
|
||||
|
||||
echo "✅ Dependencies installed"
|
||||
echo ""
|
||||
|
||||
# Step 3: Start dev server
|
||||
echo "🚀 Starting Nuxt dev server..."
|
||||
echo " App will be available at: http://localhost:3000"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop"
|
||||
echo ""
|
||||
|
||||
bun run dev
|
||||
84
docs/README.md
Normal file
84
docs/README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Pantry Documentation
|
||||
|
||||
Complete documentation for the Pantry household inventory management system.
|
||||
|
||||
## 📚 Documentation Structure
|
||||
|
||||
### 🚀 Getting Started
|
||||
|
||||
- **[Project Overview](PROJECT_PLAN.md)** - Vision, roadmap, and MVP phases
|
||||
- **[Local Setup Guide](development/local-setup.md)** - Docker Compose development environment
|
||||
- **[Getting Started](development/getting-started.md)** - Quick start for new developers
|
||||
|
||||
### 🏗️ Architecture
|
||||
|
||||
- **[Architecture Overview](architecture/overview.md)** - Tech stack, design decisions, data flow
|
||||
- **[Database Schema](architecture/database.md)** - Tables, relationships, RLS policies, migrations
|
||||
- **[API Reference](architecture/api.md)** - Supabase endpoints, Edge Functions, helpers
|
||||
|
||||
### 💻 Development
|
||||
|
||||
- **[Development Workflow](development/workflow.md)** - Daily workflow, conventions, best practices
|
||||
- **[Git Workflow](development/git-workflow.md)** - Branching strategy, PR process, reviews
|
||||
|
||||
### 🚢 Deployment
|
||||
|
||||
- **[Production Deployment](deployment/production.md)** - Docker, Coolify, environment setup
|
||||
- **[CI/CD Pipeline](deployment/ci-cd.md)** - Automated testing, builds, deployments
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Links by Role
|
||||
|
||||
### New Developer
|
||||
1. [Local Setup Guide](development/local-setup.md) - Get running in 5 minutes
|
||||
2. [Architecture Overview](architecture/overview.md) - Understand the stack
|
||||
3. [Development Workflow](development/workflow.md) - Daily development process
|
||||
|
||||
### Feature Implementation
|
||||
1. [Database Schema](architecture/database.md) - Table structure and queries
|
||||
2. [API Reference](architecture/api.md) - Available endpoints
|
||||
3. [Git Workflow](development/git-workflow.md) - Branch naming, PR checklist
|
||||
|
||||
### Deployment & Operations
|
||||
1. [Production Deployment](deployment/production.md) - Deploy to production
|
||||
2. [CI/CD Pipeline](deployment/ci-cd.md) - Automated workflows
|
||||
|
||||
---
|
||||
|
||||
## 📊 Current Status
|
||||
|
||||
**MVP Progress:** 14/34 issues complete (41.2%)
|
||||
|
||||
- ✅ Week 1: Database + Frontend Foundation (6/6)
|
||||
- ✅ Week 2: Core Inventory Management (8/8)
|
||||
- 🔄 Week 3: Barcode Scanning (1/5)
|
||||
- ⏸️ Week 4-6: Tag UI, PWA, Deployment (20 remaining)
|
||||
|
||||
See [PROJECT_PLAN.md](PROJECT_PLAN.md) for detailed roadmap.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
For contribution guidelines, see [Development Workflow](development/workflow.md).
|
||||
|
||||
Key points:
|
||||
- Feature branches off `develop`
|
||||
- PRs require review before merge
|
||||
- Follow conventional commits
|
||||
- Write tests for new features
|
||||
|
||||
---
|
||||
|
||||
## 🔗 External Resources
|
||||
|
||||
- **Repository:** https://gitea.jeanlucmakiola.de/pantry-app/pantry
|
||||
- **Supabase Docs:** https://supabase.com/docs
|
||||
- **Nuxt 4 Docs:** https://nuxt.com
|
||||
- **Open Food Facts API:** https://wiki.openfoodfacts.org/API
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-02-09
|
||||
**Version:** 0.1.0-alpha (MVP in progress)
|
||||
189
docs/development/getting-started.md
Normal file
189
docs/development/getting-started.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Getting Started with Pantry Development
|
||||
|
||||
Welcome! This guide will get you from zero to running Pantry locally in ~5 minutes.
|
||||
|
||||
## 🎯 What You'll Build
|
||||
|
||||
A self-hosted kitchen inventory app with:
|
||||
- Inventory management (add, edit, delete items)
|
||||
- Tag-based organization (Fridge, Freezer, Dairy, etc.)
|
||||
- Unit conversions (g, kg, L, cups)
|
||||
- Barcode scanning (coming soon)
|
||||
- PWA features (offline, installable)
|
||||
|
||||
## ⚡ Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Docker** & **Docker Compose** - [Install](https://docs.docker.com/get-docker/)
|
||||
- **Bun** - [Install](https://bun.sh): `curl -fsSL https://bun.sh/install | bash`
|
||||
- **Git**
|
||||
|
||||
### One-Command Setup
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://gitea.jeanlucmakiola.de/pantry-app/pantry.git
|
||||
cd pantry
|
||||
|
||||
# Run the setup script
|
||||
./dev.sh
|
||||
```
|
||||
|
||||
That's it! The script will:
|
||||
1. ✅ Start Supabase services (Docker Compose)
|
||||
2. ✅ Wait for services to initialize
|
||||
3. ✅ Install frontend dependencies
|
||||
4. ✅ Launch Nuxt dev server
|
||||
|
||||
### Access the App
|
||||
|
||||
| Service | URL | Purpose |
|
||||
|---------|-----|---------|
|
||||
| **App** | `http://localhost:3000` | Main frontend |
|
||||
| **Supabase Studio** | `http://localhost:54323` | Database admin UI |
|
||||
| **API** | `http://localhost:54321` | Backend API |
|
||||
|
||||
## 🎮 Try It Out
|
||||
|
||||
1. Open `http://localhost:3000`
|
||||
2. Click **"Add Manually"** to create your first item
|
||||
3. Fill in:
|
||||
- Name: "Milk"
|
||||
- Quantity: 1
|
||||
- Unit: Liter
|
||||
- Tags: Fridge, Dairy
|
||||
- Expiry: Set a date
|
||||
4. Click **"Add Item"** and see it in the grid!
|
||||
|
||||
### Explore Features
|
||||
|
||||
- **Edit item:** Click "Edit" on any card
|
||||
- **Adjust quantity:** Use +/- buttons
|
||||
- **Delete item:** Click "Delete" (confirms first)
|
||||
- **View database:** Open Supabase Studio at `:54323`
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
pantry/
|
||||
├── app/ # Nuxt 4 frontend
|
||||
│ ├── components/ # Vue components
|
||||
│ │ └── inventory/ # Inventory UI (List, Card, Forms)
|
||||
│ ├── composables/ # Data hooks (useInventory, useSupabase)
|
||||
│ ├── pages/ # Routes (index, scan, settings)
|
||||
│ └── types/ # TypeScript definitions
|
||||
├── supabase/
|
||||
│ └── migrations/ # Database schema (001-005)
|
||||
├── docker-compose.yml # Supabase services
|
||||
├── docker/
|
||||
│ └── kong.yml # API gateway config
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
## 🛠️ Common Tasks
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# All services
|
||||
docker-compose logs -f
|
||||
|
||||
# Just the database
|
||||
docker-compose logs -f db
|
||||
```
|
||||
|
||||
### Reset Database
|
||||
|
||||
```bash
|
||||
# Stop and remove volumes (fresh start)
|
||||
docker-compose down -v
|
||||
|
||||
# Restart (migrations auto-apply)
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Access Database Directly
|
||||
|
||||
```bash
|
||||
# psql CLI
|
||||
docker-compose exec db psql -U postgres -d postgres
|
||||
|
||||
# Or use Supabase Studio (GUI)
|
||||
open http://localhost:54323
|
||||
```
|
||||
|
||||
### Stop Everything
|
||||
|
||||
```bash
|
||||
# Stop services (keep data)
|
||||
docker-compose stop
|
||||
|
||||
# Stop and remove everything
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## 🔍 What's Included
|
||||
|
||||
### Database (Pre-seeded)
|
||||
|
||||
**30 Units:**
|
||||
- Weight: g, kg, mg, lb, oz
|
||||
- Volume: mL, L, cup, tbsp, tsp
|
||||
- Count: piece, dozen, bottle, can, jar
|
||||
|
||||
**33 Tags:**
|
||||
- Position: Fridge, Freezer, Pantry
|
||||
- Type: Dairy, Meat, Vegetables, Fruits
|
||||
- Dietary: Vegan, Gluten-Free, Organic
|
||||
- Custom: Low Stock, To Buy, Meal Prep
|
||||
|
||||
### Features (Working Now)
|
||||
|
||||
- ✅ Add/Edit/Delete inventory items
|
||||
- ✅ Tag selection (multi-select)
|
||||
- ✅ Unit conversions
|
||||
- ✅ Expiry date tracking with warnings
|
||||
- ✅ Responsive layout (mobile-ready)
|
||||
- ✅ Quantity quick actions (+/- buttons)
|
||||
|
||||
### Features (Coming Soon)
|
||||
|
||||
- ⏳ Barcode scanning (Week 3)
|
||||
- ⏳ User authentication UI
|
||||
- ⏳ Tag management
|
||||
- ⏳ PWA (offline mode)
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
### Learn the Stack
|
||||
|
||||
1. **[Architecture Overview](../architecture/overview.md)** - Tech stack and design decisions
|
||||
2. **[Database Schema](../architecture/database.md)** - Tables and relationships
|
||||
3. **[Development Workflow](workflow.md)** - Daily development process
|
||||
|
||||
### Make Your First Change
|
||||
|
||||
1. Pick an issue from Gitea
|
||||
2. Create a branch: `git checkout -b feature/your-feature`
|
||||
3. Make changes, test locally
|
||||
4. Commit: `git commit -m "feat: your feature"`
|
||||
5. Push and create PR
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
See **[Local Setup Guide](local-setup.md)** for:
|
||||
- Port conflicts
|
||||
- Database connection issues
|
||||
- Frontend errors
|
||||
- Environment variables
|
||||
|
||||
## 🤝 Need Help?
|
||||
|
||||
- **Documentation:** Browse `/docs` folder
|
||||
- **Issues:** Create an issue on Gitea
|
||||
- **Local setup:** See [local-setup.md](local-setup.md)
|
||||
|
||||
---
|
||||
|
||||
**Ready to code?** Check out the [Development Workflow](workflow.md)!
|
||||
83
supabase/functions/product-lookup/README.md
Normal file
83
supabase/functions/product-lookup/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Product Lookup Edge Function
|
||||
|
||||
Fetches product data from Open Food Facts API by barcode and caches results in the database.
|
||||
|
||||
## Endpoint
|
||||
|
||||
`POST /functions/v1/product-lookup`
|
||||
|
||||
## Request
|
||||
|
||||
```json
|
||||
{
|
||||
"barcode": "8000500310427"
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
### Success (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"barcode": "8000500310427",
|
||||
"name": "Nutella",
|
||||
"brand": "Ferrero",
|
||||
"quantity": "750g",
|
||||
"image_url": "https://...",
|
||||
"category": "spreads",
|
||||
"cached": false
|
||||
}
|
||||
```
|
||||
|
||||
### Not Found (404)
|
||||
|
||||
```json
|
||||
{
|
||||
"barcode": "1234567890123",
|
||||
"name": "Unknown Product (1234567890123)",
|
||||
"cached": false
|
||||
}
|
||||
```
|
||||
|
||||
### Error (500)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Error message",
|
||||
"barcode": null,
|
||||
"name": null
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Queries Open Food Facts API
|
||||
- ✅ Caches results in `products` table
|
||||
- ✅ Returns cached data for subsequent requests
|
||||
- ✅ Handles product not found gracefully
|
||||
- ✅ CORS enabled for frontend access
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `SUPABASE_URL`: Auto-injected by Supabase
|
||||
- `SUPABASE_SERVICE_ROLE_KEY`: Auto-injected by Supabase
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Local (with Supabase CLI)
|
||||
supabase functions serve product-lookup
|
||||
|
||||
# Test request
|
||||
curl -X POST http://localhost:54321/functions/v1/product-lookup \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_ANON_KEY" \
|
||||
-d '{"barcode":"8000500310427"}'
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
supabase functions deploy product-lookup
|
||||
```
|
||||
140
supabase/functions/product-lookup/index.ts
Normal file
140
supabase/functions/product-lookup/index.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
// Product Lookup Edge Function
|
||||
// Fetches product data from Open Food Facts API by barcode
|
||||
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
|
||||
interface ProductData {
|
||||
barcode: string
|
||||
name: string
|
||||
brand?: string
|
||||
quantity?: string
|
||||
image_url?: string
|
||||
category?: string
|
||||
cached?: boolean
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
// Handle CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
try {
|
||||
const { barcode } = await req.json()
|
||||
|
||||
if (!barcode) {
|
||||
throw new Error('Barcode is required')
|
||||
}
|
||||
|
||||
// Initialize Supabase client
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
|
||||
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
const supabase = createClient(supabaseUrl, supabaseKey)
|
||||
|
||||
// Check cache first (products table can store known products)
|
||||
const { data: cachedProduct } = await supabase
|
||||
.from('products')
|
||||
.select('*')
|
||||
.eq('barcode', barcode)
|
||||
.single()
|
||||
|
||||
if (cachedProduct) {
|
||||
console.log(`Cache HIT for barcode: ${barcode}`)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
barcode: cachedProduct.barcode,
|
||||
name: cachedProduct.name,
|
||||
brand: cachedProduct.brand,
|
||||
quantity: cachedProduct.quantity,
|
||||
image_url: cachedProduct.image_url,
|
||||
category: cachedProduct.category,
|
||||
cached: true,
|
||||
} as ProductData),
|
||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Cache MISS for barcode: ${barcode}, fetching from Open Food Facts...`)
|
||||
|
||||
// Fetch from Open Food Facts
|
||||
const offResponse = await fetch(
|
||||
`https://world.openfoodfacts.org/api/v2/product/${barcode}.json`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'Pantry/1.0 (https://github.com/pantry-app/pantry)',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!offResponse.ok) {
|
||||
throw new Error(`Open Food Facts API error: ${offResponse.status}`)
|
||||
}
|
||||
|
||||
const offData = await offResponse.json()
|
||||
|
||||
if (offData.status !== 1 || !offData.product) {
|
||||
// Product not found in Open Food Facts
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
barcode,
|
||||
name: `Unknown Product (${barcode})`,
|
||||
cached: false,
|
||||
} as ProductData),
|
||||
{
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 404
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const product = offData.product
|
||||
|
||||
// Extract relevant data
|
||||
const productData: ProductData = {
|
||||
barcode,
|
||||
name: product.product_name || product.generic_name || `Product ${barcode}`,
|
||||
brand: product.brands || undefined,
|
||||
quantity: product.quantity || undefined,
|
||||
image_url: product.image_url || product.image_front_url || undefined,
|
||||
category: product.categories || undefined,
|
||||
cached: false,
|
||||
}
|
||||
|
||||
// Cache the product in our database (upsert)
|
||||
await supabase.from('products').upsert({
|
||||
barcode: productData.barcode,
|
||||
name: productData.name,
|
||||
brand: productData.brand,
|
||||
quantity: productData.quantity,
|
||||
image_url: productData.image_url,
|
||||
category: productData.category,
|
||||
}, { onConflict: 'barcode' })
|
||||
|
||||
console.log(`Successfully fetched and cached product: ${productData.name}`)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify(productData),
|
||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in product-lookup:', error)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
barcode: null,
|
||||
name: null,
|
||||
}),
|
||||
{
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 500
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user