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 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

View file

@ -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

View file

@ -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>
)
}