Fix review display: add avatar, username, localized time, granular score

This commit is contained in:
fullsizemalt 2025-12-26 18:14:47 -08:00
parent 36d6fbfad9
commit 5b7d8da250
3 changed files with 81 additions and 23 deletions

View file

@ -2,7 +2,7 @@ from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session, select, func from sqlmodel import Session, select, func
from database import get_session from database import get_session
from models import Review, User from models import Review, User, Profile
from schemas import ReviewCreate, ReviewRead from schemas import ReviewCreate, ReviewRead
from auth import get_current_user from auth import get_current_user
from services.gamification import award_xp, check_and_award_badges, update_streak, XP_REWARDS 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) session.refresh(db_review)
return db_review return db_review
@router.get("/", response_model=List[ReviewRead]) @router.get("/")
def read_reviews( def read_reviews(
show_id: Optional[int] = None, show_id: Optional[int] = None,
venue_id: Optional[int] = None, venue_id: Optional[int] = None,
@ -70,4 +70,24 @@ def read_reviews(
query = query.where(Review.year == year) query = query.where(Review.year == year)
reviews = session.exec(query.offset(offset).limit(limit)).all() 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

View file

@ -240,7 +240,7 @@ class CommentRead(CommentBase):
# We might want to include the username here later # We might want to include the username here later
class RatingBase(SQLModel): class RatingBase(SQLModel):
score: float score: Optional[float] = None # 1-5 stars, optional
show_id: Optional[int] = None show_id: Optional[int] = None
song_id: Optional[int] = None song_id: Optional[int] = None
performance_id: Optional[int] = None performance_id: Optional[int] = None
@ -256,9 +256,9 @@ class RatingRead(RatingBase):
created_at: datetime created_at: datetime
class ReviewBase(SQLModel): class ReviewBase(SQLModel):
blurb: str blurb: Optional[str] = None # Short tagline/summary
content: str content: Optional[str] = None # Full review text
score: float score: Optional[float] = None # Optional rating with review
show_id: Optional[int] = None show_id: Optional[int] = None
venue_id: Optional[int] = None venue_id: Optional[int] = None
song_id: Optional[int] = None song_id: Optional[int] = None

View file

@ -1,14 +1,24 @@
"use client"
import { Card, CardContent, CardHeader } from "@/components/ui/card" import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { StarRating } from "@/components/ui/star-rating" import { UserAvatar } from "@/components/ui/user-avatar"
import { formatDistanceToNow } from "date-fns"
interface ReviewUser {
id: number
username: string
display_name?: string | null
avatar_bg_color?: string
avatar_text?: string | null
}
interface Review { interface Review {
id: number id: number
user_id: number user_id: number
blurb: string blurb?: string | null
content: string content?: string | null
score: number score?: number | null
created_at: string created_at: string
user?: ReviewUser | null
} }
interface ReviewCardProps { interface ReviewCardProps {
@ -16,24 +26,52 @@ interface ReviewCardProps {
} }
export function ReviewCard({ review }: 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 ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex justify-between items-start"> <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"> <div className="space-y-1">
{review.blurb && (
<h3 className="font-bold text-lg italic">"{review.blurb}"</h3> <h3 className="font-bold text-lg italic">"{review.blurb}"</h3>
)}
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>User #{review.user_id}</span> <span className="font-medium text-foreground">{username}</span>
<span></span> <span></span>
<span>{formatDistanceToNow(new Date(review.created_at), { addSuffix: true })}</span> <span>{formattedDate}</span>
</div> </div>
</div> </div>
<StarRating value={review.score} readonly /> </div>
{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> </div>
</CardHeader> </CardHeader>
{review.content && (
<CardContent> <CardContent>
<p className="text-sm leading-relaxed whitespace-pre-wrap">{review.content}</p> <p className="text-sm leading-relaxed whitespace-pre-wrap">{review.content}</p>
</CardContent> </CardContent>
)}
</Card> </Card>
) )
} }