Fix review display: add avatar, username, localized time, granular score
This commit is contained in:
parent
36d6fbfad9
commit
5b7d8da250
3 changed files with 81 additions and 23 deletions
|
|
@ -2,7 +2,7 @@ from typing import List, Optional
|
|||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlmodel import Session, select, func
|
||||
from database import get_session
|
||||
from models import Review, User
|
||||
from models import Review, User, Profile
|
||||
from schemas import ReviewCreate, ReviewRead
|
||||
from auth import get_current_user
|
||||
from services.gamification import award_xp, check_and_award_badges, update_streak, XP_REWARDS
|
||||
|
|
@ -43,7 +43,7 @@ def create_review(
|
|||
session.refresh(db_review)
|
||||
return db_review
|
||||
|
||||
@router.get("/", response_model=List[ReviewRead])
|
||||
@router.get("/")
|
||||
def read_reviews(
|
||||
show_id: Optional[int] = None,
|
||||
venue_id: Optional[int] = None,
|
||||
|
|
@ -70,4 +70,24 @@ def read_reviews(
|
|||
query = query.where(Review.year == year)
|
||||
|
||||
reviews = session.exec(query.offset(offset).limit(limit)).all()
|
||||
return reviews
|
||||
|
||||
# Enrich with user profile data
|
||||
result = []
|
||||
for review in reviews:
|
||||
user = session.get(User, review.user_id)
|
||||
profile = session.exec(
|
||||
select(Profile).where(Profile.user_id == review.user_id)
|
||||
).first()
|
||||
|
||||
result.append({
|
||||
**review.model_dump(),
|
||||
"user": {
|
||||
"id": user.id if user else review.user_id,
|
||||
"username": profile.username if profile else f"User {review.user_id}",
|
||||
"display_name": profile.display_name if profile else None,
|
||||
"avatar_bg_color": user.avatar_bg_color if user else "#0F4C81",
|
||||
"avatar_text": user.avatar_text if user else None,
|
||||
} if user else None
|
||||
})
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ class CommentRead(CommentBase):
|
|||
# We might want to include the username here later
|
||||
|
||||
class RatingBase(SQLModel):
|
||||
score: float
|
||||
score: Optional[float] = None # 1-5 stars, optional
|
||||
show_id: Optional[int] = None
|
||||
song_id: Optional[int] = None
|
||||
performance_id: Optional[int] = None
|
||||
|
|
@ -256,9 +256,9 @@ class RatingRead(RatingBase):
|
|||
created_at: datetime
|
||||
|
||||
class ReviewBase(SQLModel):
|
||||
blurb: str
|
||||
content: str
|
||||
score: float
|
||||
blurb: Optional[str] = None # Short tagline/summary
|
||||
content: Optional[str] = None # Full review text
|
||||
score: Optional[float] = None # Optional rating with review
|
||||
show_id: Optional[int] = None
|
||||
venue_id: Optional[int] = None
|
||||
song_id: Optional[int] = None
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||
import { StarRating } from "@/components/ui/star-rating"
|
||||
import { formatDistanceToNow } from "date-fns"
|
||||
import { UserAvatar } from "@/components/ui/user-avatar"
|
||||
|
||||
interface ReviewUser {
|
||||
id: number
|
||||
username: string
|
||||
display_name?: string | null
|
||||
avatar_bg_color?: string
|
||||
avatar_text?: string | null
|
||||
}
|
||||
|
||||
interface Review {
|
||||
id: number
|
||||
user_id: number
|
||||
blurb: string
|
||||
content: string
|
||||
score: number
|
||||
blurb?: string | null
|
||||
content?: string | null
|
||||
score?: number | null
|
||||
created_at: string
|
||||
user?: ReviewUser | null
|
||||
}
|
||||
|
||||
interface ReviewCardProps {
|
||||
|
|
@ -16,24 +26,52 @@ interface ReviewCardProps {
|
|||
}
|
||||
|
||||
export function ReviewCard({ review }: ReviewCardProps) {
|
||||
// Format date in user's locale
|
||||
const formattedDate = new Date(review.created_at).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
})
|
||||
|
||||
const username = review.user?.display_name || review.user?.username || `User ${review.user_id}`
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-bold text-lg italic">"{review.blurb}"</h3>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>User #{review.user_id}</span>
|
||||
<span>•</span>
|
||||
<span>{formatDistanceToNow(new Date(review.created_at), { addSuffix: true })}</span>
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<UserAvatar
|
||||
bgColor={review.user?.avatar_bg_color || "#0F4C81"}
|
||||
text={review.user?.avatar_text || undefined}
|
||||
username={username}
|
||||
size="sm"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
{review.blurb && (
|
||||
<h3 className="font-bold text-lg italic">"{review.blurb}"</h3>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">{username}</span>
|
||||
<span>•</span>
|
||||
<span>{formattedDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<StarRating value={review.score} readonly />
|
||||
{review.score !== null && review.score !== undefined && (
|
||||
<div className="flex items-center gap-1 bg-primary/10 px-2 py-1 rounded-md">
|
||||
<span className="text-lg font-bold text-primary">{review.score.toFixed(1)}</span>
|
||||
<span className="text-xs text-muted-foreground">/10</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm leading-relaxed whitespace-pre-wrap">{review.content}</p>
|
||||
</CardContent>
|
||||
{review.content && (
|
||||
<CardContent>
|
||||
<p className="text-sm leading-relaxed whitespace-pre-wrap">{review.content}</p>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue