Initial project scaffold
Full-stack Dutch supermarket price tracker with FastAPI backend, PostgreSQL/SQLAlchemy, Albert Heijn scraper, and Next.js frontend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
# Dutch Food Price Tracker
|
||||
|
||||
Track Dutch supermarket food prices over time. Currently supports Albert Heijn.
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Tech |
|
||||
|---|---|
|
||||
| Backend API | Python FastAPI + SQLAlchemy + Alembic |
|
||||
| Database | PostgreSQL 16 |
|
||||
| Scraper | httpx + AH anonymous token auth |
|
||||
| Frontend | Next.js 14 + TypeScript + Tailwind CSS + Recharts |
|
||||
| Dev infra | Docker Compose |
|
||||
|
||||
## Quick start (Docker)
|
||||
|
||||
```bash
|
||||
git clone <repo> && cd dutch-food-price-tracker
|
||||
|
||||
# Build and start all services
|
||||
docker compose up --build -d
|
||||
|
||||
# Run DB migrations (tables are also auto-created on backend start)
|
||||
docker compose exec backend alembic upgrade head
|
||||
|
||||
# Scrape Albert Heijn
|
||||
docker compose exec backend python cli.py scrape-ah \
|
||||
--query melk \
|
||||
--query brood \
|
||||
--query kaas \
|
||||
--query yoghurt
|
||||
```
|
||||
|
||||
- Frontend: http://localhost:3000
|
||||
- API docs: http://localhost:8000/docs
|
||||
- Database: `localhost:5432` (user: postgres / password: postgres / db: food_prices)
|
||||
|
||||
## Local development (without Docker)
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Needs a running PostgreSQL instance
|
||||
export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/food_prices
|
||||
|
||||
# Create tables
|
||||
alembic upgrade head
|
||||
# or: python -c "from app.database import engine; from app.models import Base; Base.metadata.create_all(engine)"
|
||||
|
||||
# Start API server
|
||||
uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
|
||||
# Point at the local backend
|
||||
echo "NEXT_PUBLIC_API_URL=http://localhost:8000" > .env.local
|
||||
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## CLI reference
|
||||
|
||||
```bash
|
||||
# Scrape one or more queries
|
||||
python cli.py scrape-ah --query "melk" --query "brood"
|
||||
|
||||
# Run via Docker
|
||||
docker compose exec backend python cli.py scrape-ah --query melk
|
||||
```
|
||||
|
||||
## API endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/products?search=melk` | Search products |
|
||||
| GET | `/api/products/{id}` | Product detail |
|
||||
| GET | `/api/products/{id}/prices` | Full price history |
|
||||
| GET | `/api/prices/cheapest?date=2024-01-15` | Cheapest per product for a day |
|
||||
| GET | `/api/stores` | List stores |
|
||||
| GET | `/api/scrape-runs` | List recent scrape runs |
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
dutch-food-price-tracker/
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── main.py # FastAPI app + CORS
|
||||
│ │ ├── models.py # SQLAlchemy ORM models
|
||||
│ │ ├── schemas.py # Pydantic request/response schemas
|
||||
│ │ ├── database.py # Engine, session, Base
|
||||
│ │ ├── config.py # Pydantic settings
|
||||
│ │ ├── routers/ # products, stores, prices, scrape_runs
|
||||
│ │ └── scrapers/
|
||||
│ │ └── albert_heijn.py # Token auth + product search
|
||||
│ ├── alembic/ # DB migration history
|
||||
│ ├── cli.py # Click CLI (scrape-ah)
|
||||
│ └── requirements.txt
|
||||
├── frontend/
|
||||
│ └── src/
|
||||
│ ├── app/
|
||||
│ │ ├── page.tsx # Product search
|
||||
│ │ ├── products/[id]/page.tsx # Detail + price chart
|
||||
│ │ └── cheapest/page.tsx # Daily cheapest overview
|
||||
│ ├── components/
|
||||
│ │ ├── Nav.tsx
|
||||
│ │ ├── ProductCard.tsx
|
||||
│ │ └── PriceChart.tsx # Recharts line chart
|
||||
│ └── lib/api.ts # Typed API client
|
||||
├── seed/ # Historical CSV/JSON import datasets
|
||||
├── docker-compose.yml
|
||||
└── .env.example
|
||||
```
|
||||
|
||||
## Adding a new store
|
||||
|
||||
1. Create `backend/app/scrapers/<store_slug>.py`
|
||||
2. Implement `scrape_query(db: Session, query: str) -> ScrapeRun`
|
||||
3. Add a `@cli.command` in `backend/cli.py`
|
||||
4. Insert a `Store` row with the new slug
|
||||
|
||||
## Data model
|
||||
|
||||
- **stores** — one row per chain (Albert Heijn, Jumbo, …)
|
||||
- **products** — one row per store SKU; keyed by `(store_id, external_id)`
|
||||
- **scrape_runs** — one row per CLI invocation / query
|
||||
- **price_snapshots** — append-only price observations (cents, UTC timestamp)
|
||||
Reference in New Issue
Block a user