feat: Add Findagram and FindADispo consumer frontends

- Add findagram.co React frontend with product search, brands, categories
- Add findadispo.com React frontend with dispensary locator
- Wire findagram to backend /api/az/* endpoints
- Update category/brand links to route to /products with filters
- Add k8s manifests for both frontends
- Add multi-domain user support migrations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-05 16:10:15 -07:00
parent d120a07ed7
commit a0f8d3911c
179 changed files with 140234 additions and 600 deletions

View File

@@ -0,0 +1,164 @@
from typing import Optional
from fastapi import APIRouter, HTTPException
import httpx
from pydantic import BaseModel
from config import get_settings
router = APIRouter(prefix="/dispensaries", tags=["Dispensaries"])
settings = get_settings()
class DispensaryQuery(BaseModel):
lat: Optional[float] = None
lng: Optional[float] = None
city: Optional[str] = None
state: Optional[str] = None
radius: Optional[int] = 25
limit: Optional[int] = 20
offset: Optional[int] = 0
async def fetch_from_api(endpoint: str, params: dict = None):
"""Fetch data from the external dispensary API"""
headers = {}
if settings.dispensary_api_key:
headers["X-API-Key"] = settings.dispensary_api_key
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{settings.dispensary_api_url}{endpoint}",
params=params,
headers=headers,
timeout=30.0
)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
raise HTTPException(
status_code=e.response.status_code,
detail=f"API error: {e.response.text}"
)
except httpx.RequestError as e:
raise HTTPException(
status_code=503,
detail=f"Failed to connect to dispensary API: {str(e)}"
)
@router.get("/")
async def search_dispensaries(
lat: Optional[float] = None,
lng: Optional[float] = None,
city: Optional[str] = None,
state: Optional[str] = "AZ",
radius: int = 25,
limit: int = 20,
offset: int = 0,
open_now: bool = False,
min_rating: Optional[float] = None
):
"""Search for dispensaries by location"""
params = {
"limit": limit,
"offset": offset,
"state": state
}
if lat and lng:
params["lat"] = lat
params["lng"] = lng
params["radius"] = radius
if city:
params["city"] = city
# Fetch from external API
data = await fetch_from_api("/api/az/stores", params)
# Apply client-side filters if needed
stores = data.get("stores", [])
if open_now:
# Filter stores that are currently open
# This would need actual business hours logic
pass
if min_rating:
stores = [s for s in stores if (s.get("rating") or 0) >= min_rating]
return {
"dispensaries": stores,
"total": len(stores),
"limit": limit,
"offset": offset
}
@router.get("/{dispensary_id}")
async def get_dispensary(dispensary_id: int):
"""Get details for a specific dispensary"""
data = await fetch_from_api(f"/api/az/stores/{dispensary_id}")
return data
@router.get("/{dispensary_id}/products")
async def get_dispensary_products(
dispensary_id: int,
category: Optional[str] = None,
search: Optional[str] = None,
limit: int = 50,
offset: int = 0
):
"""Get products for a specific dispensary"""
params = {
"limit": limit,
"offset": offset
}
if category:
params["category"] = category
if search:
params["search"] = search
data = await fetch_from_api(f"/api/az/stores/{dispensary_id}/products", params)
return data
@router.get("/{dispensary_id}/categories")
async def get_dispensary_categories(dispensary_id: int):
"""Get product categories for a dispensary"""
data = await fetch_from_api(f"/api/az/stores/{dispensary_id}/categories")
return data
@router.get("/nearby")
async def get_nearby_dispensaries(
lat: float,
lng: float,
radius: int = 10,
limit: int = 10
):
"""Get nearby dispensaries by coordinates"""
params = {
"lat": lat,
"lng": lng,
"radius": radius,
"limit": limit
}
data = await fetch_from_api("/api/az/stores", params)
return data.get("stores", [])
@router.get("/featured")
async def get_featured_dispensaries(limit: int = 6):
"""Get featured dispensaries for the homepage"""
# For now, return top-rated dispensaries
params = {
"limit": limit,
"sort": "rating"
}
data = await fetch_from_api("/api/az/stores", params)
return data.get("stores", [])