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 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue