Files
tasteby/backend/api/routes/reviews.py
joungmin 3694730501 Add admin features, responsive UI, user reviews, visit stats, and channel-colored markers
- Admin: video management with Google Maps match status, manual restaurant mapping, restaurant remap on name change
- Admin: user management tab with favorites/reviews detail
- Admin: channel deletion fix for IDs with slashes
- Frontend: responsive mobile layout (map top, list bottom, 2-row header)
- Frontend: channel-colored map markers with legend
- Frontend: my reviews list, favorites toggle, visit counter overlay
- Frontend: force light mode for dark theme devices
- Backend: visit tracking (site_visits table), user reviews endpoint
- Backend: bulk transcript/extract streaming, geocode key fixes
- Nginx config for production deployment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 14:52:20 +09:00

169 lines
5.0 KiB
Python

"""Review API routes."""
from __future__ import annotations
from datetime import date
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from core import review
from api.deps import get_current_user
router = APIRouter()
class ReviewCreate(BaseModel):
rating: float = Field(..., ge=0.5, le=5.0)
review_text: Optional[str] = None
visited_at: Optional[date] = None
class ReviewUpdate(BaseModel):
rating: Optional[float] = Field(None, ge=0.5, le=5.0)
review_text: Optional[str] = None
visited_at: Optional[date] = None
# --- Restaurant reviews ---
@router.get("/restaurants/{restaurant_id}/reviews")
def list_restaurant_reviews(
restaurant_id: str,
limit: int = Query(20, le=100),
offset: int = Query(0, ge=0),
):
"""List reviews for a restaurant (public)."""
reviews = review.get_reviews_for_restaurant(restaurant_id, limit=limit, offset=offset)
stats = review.get_restaurant_avg_rating(restaurant_id)
return {"reviews": reviews, **stats}
@router.post("/restaurants/{restaurant_id}/reviews", status_code=201)
def create_restaurant_review(
restaurant_id: str,
body: ReviewCreate,
current_user: dict = Depends(get_current_user),
):
"""Create a review for a restaurant (requires auth)."""
created = review.create_review(
user_id=current_user["sub"],
restaurant_id=restaurant_id,
rating=body.rating,
review_text=body.review_text,
visited_at=body.visited_at,
)
return created
@router.put("/reviews/{review_id}")
def update_review_route(
review_id: str,
body: ReviewUpdate,
current_user: dict = Depends(get_current_user),
):
"""Update a review (own review only)."""
updated = review.update_review(
review_id=review_id,
user_id=current_user["sub"],
rating=body.rating,
review_text=body.review_text,
visited_at=body.visited_at,
)
if not updated:
raise HTTPException(404, "Review not found or not yours")
return updated
@router.delete("/reviews/{review_id}", status_code=204)
def delete_review_route(
review_id: str,
current_user: dict = Depends(get_current_user),
):
"""Delete a review (own review only)."""
deleted = review.delete_review(review_id, current_user["sub"])
if not deleted:
raise HTTPException(404, "Review not found or not yours")
@router.get("/users/me/reviews")
def list_my_reviews(
limit: int = Query(20, le=100),
offset: int = Query(0, ge=0),
current_user: dict = Depends(get_current_user),
):
"""List current user's reviews."""
return review.get_user_reviews(current_user["sub"], limit=limit, offset=offset)
# --- Favorites ---
@router.get("/restaurants/{restaurant_id}/favorite")
def get_favorite_status(
restaurant_id: str,
current_user: dict = Depends(get_current_user),
):
"""Check if current user has favorited this restaurant."""
from core.db import conn
with conn() as c:
cur = c.cursor()
cur.execute(
"SELECT id FROM user_favorites WHERE user_id = :u AND restaurant_id = :r",
{"u": current_user["sub"], "r": restaurant_id},
)
return {"favorited": cur.fetchone() is not None}
@router.post("/restaurants/{restaurant_id}/favorite")
def toggle_favorite(
restaurant_id: str,
current_user: dict = Depends(get_current_user),
):
"""Toggle favorite. Returns new state."""
from core.db import conn
user_id = current_user["sub"]
with conn() as c:
cur = c.cursor()
cur.execute(
"SELECT id FROM user_favorites WHERE user_id = :u AND restaurant_id = :r",
{"u": user_id, "r": restaurant_id},
)
row = cur.fetchone()
if row:
cur.execute("DELETE FROM user_favorites WHERE id = :fid", {"fid": row[0]})
return {"favorited": False}
else:
cur.execute(
"INSERT INTO user_favorites (user_id, restaurant_id) VALUES (:u, :r)",
{"u": user_id, "r": restaurant_id},
)
return {"favorited": True}
@router.get("/users/me/favorites")
def list_my_favorites(
current_user: dict = Depends(get_current_user),
):
"""List current user's favorite restaurants."""
from core.db import conn
sql = """
SELECT r.id, r.name, r.address, r.region, r.latitude, r.longitude,
r.cuisine_type, r.price_range, r.google_place_id,
r.business_status, r.rating, r.rating_count,
f.created_at
FROM user_favorites f
JOIN restaurants r ON r.id = f.restaurant_id
WHERE f.user_id = :u
ORDER BY f.created_at DESC
"""
with conn() as c:
cur = c.cursor()
cur.execute(sql, {"u": current_user["sub"]})
cols = [d[0].lower() for d in cur.description]
rows = [dict(zip(cols, row)) for row in cur.fetchall()]
for r in rows:
if r.get("created_at"):
r["created_at"] = r["created_at"].isoformat()
return rows