From 60b5cb9961225dad098d004ee041c480c046d1f2 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Fri, 26 Dec 2025 19:20:17 -0800 Subject: [PATCH] Enhance ratings: show user's rating, fix username consistency, add activity links --- backend/routers/feed.py | 95 +++++++++++++------- backend/routers/social.py | 28 ++++++ frontend/components/feed/activity-feed.tsx | 92 +++++++++++++++++-- frontend/components/social/entity-rating.tsx | 85 +++++++++++++++--- 4 files changed, 247 insertions(+), 53 deletions(-) diff --git a/backend/routers/feed.py b/backend/routers/feed.py index f9dc0f8..87c189c 100644 --- a/backend/routers/feed.py +++ b/backend/routers/feed.py @@ -2,7 +2,7 @@ from typing import List, Union from fastapi import APIRouter, Depends, Query from sqlmodel import Session, select, desc from database import get_session -from models import Review, Attendance, GroupPost, User +from models import Review, Attendance, GroupPost, User, Profile, Performance, Show, Song from schemas import ReviewRead, AttendanceRead, GroupPostRead from datetime import datetime @@ -16,6 +16,56 @@ class FeedItem(BaseModel): timestamp: datetime data: Union[ReviewRead, AttendanceRead, GroupPostRead, dict] user: dict # Basic user info + entity: dict | None = None # Linked entity info + +def get_user_display(session: Session, user_id: int) -> dict: + """Get consistent user display info using Profile username""" + user = session.get(User, user_id) + if not user: + return {"id": 0, "username": "Deleted User", "avatar_bg_color": "#666", "avatar_text": None} + + profile = session.exec( + select(Profile).where(Profile.user_id == user_id) + ).first() + + return { + "id": user.id, + "username": profile.username if profile else f"User {user_id}", + "display_name": profile.display_name if profile else None, + "avatar_bg_color": user.avatar_bg_color or "#0F4C81", + "avatar_text": user.avatar_text, + } + +def get_entity_info(session: Session, review: Review) -> dict | None: + """Get entity link info for a review""" + if review.performance_id: + perf = session.get(Performance, review.performance_id) + if perf: + song = session.get(Song, perf.song_id) + show = session.get(Show, perf.show_id) + return { + "type": "performance", + "slug": perf.slug, + "title": song.title if song else "Unknown Song", + "date": show.date.isoformat() if show and show.date else None, + } + elif review.show_id: + show = session.get(Show, review.show_id) + if show: + return { + "type": "show", + "slug": show.slug, + "title": show.date.strftime("%Y-%m-%d") if show.date else "Unknown Date", + } + elif review.song_id: + song = session.get(Song, review.song_id) + if song: + return { + "type": "song", + "slug": song.slug, + "title": song.title, + } + return None @router.get("/", response_model=List[FeedItem]) def get_global_feed( @@ -40,54 +90,39 @@ def get_global_feed( feed_items = [] for r in reviews: - user = session.get(User, r.user_id) - user_data = {"id": 0, "username": "Deleted User", "avatar": None} - if user: - user_data = { - "id": user.id, - "username": user.email.split("@")[0] if user.email else "User", - "avatar": user.avatar - } - feed_items.append(FeedItem( type="review", timestamp=r.created_at or datetime.utcnow(), data=r, - user=user_data + user=get_user_display(session, r.user_id), + entity=get_entity_info(session, r) )) for a in attendance: - user = session.get(User, a.user_id) - user_data = {"id": 0, "username": "Deleted User", "avatar": None} - if user: - user_data = { - "id": user.id, - "username": user.email.split("@")[0] if user.email else "User", - "avatar": user.avatar + show = session.get(Show, a.show_id) if a.show_id else None + entity_info = None + if show: + entity_info = { + "type": "show", + "slug": show.slug, + "title": show.date.strftime("%Y-%m-%d") if show.date else "Unknown", } - + feed_items.append(FeedItem( type="attendance", timestamp=a.created_at, data=a, - user=user_data + user=get_user_display(session, a.user_id), + entity=entity_info )) for p in posts: - user = session.get(User, p.user_id) - user_data = {"id": 0, "username": "Deleted User", "avatar": None} - if user: - user_data = { - "id": user.id, - "username": user.email.split("@")[0] if user.email else "User", - "avatar": user.avatar - } - feed_items.append(FeedItem( type="post", timestamp=p.created_at, data=p, - user=user_data + user=get_user_display(session, p.user_id), + entity=None )) # Sort by timestamp desc diff --git a/backend/routers/social.py b/backend/routers/social.py index a46a56e..524a6ec 100644 --- a/backend/routers/social.py +++ b/backend/routers/social.py @@ -161,6 +161,34 @@ def get_average_rating( avg = session.exec(query).first() return float(avg) if avg else 0.0 +@router.get("/ratings/me") +def get_my_rating( + show_id: Optional[int] = None, + song_id: Optional[int] = None, + performance_id: Optional[int] = None, + venue_id: Optional[int] = None, + tour_id: Optional[int] = None, + current_user: User = Depends(get_current_user), + session: Session = Depends(get_session) +): + """Get current user's rating for an entity""" + query = select(Rating).where(Rating.user_id == current_user.id) + if show_id: + query = query.where(Rating.show_id == show_id) + elif song_id: + query = query.where(Rating.song_id == song_id) + elif performance_id: + query = query.where(Rating.performance_id == performance_id) + elif venue_id: + query = query.where(Rating.venue_id == venue_id) + elif tour_id: + query = query.where(Rating.tour_id == tour_id) + else: + return None + + rating = session.exec(query).first() + return {"score": rating.score if rating else None, "id": rating.id if rating else None} + # --- Reactions --- @router.post("/reactions", response_model=ReactionRead) diff --git a/frontend/components/feed/activity-feed.tsx b/frontend/components/feed/activity-feed.tsx index 10e8bc4..2213b41 100644 --- a/frontend/components/feed/activity-feed.tsx +++ b/frontend/components/feed/activity-feed.tsx @@ -6,6 +6,14 @@ import { Card, CardContent } from "@/components/ui/card" import { Calendar, MessageSquare, Star, Users } from "lucide-react" import Link from "next/link" import { WikiText } from "@/components/ui/wiki-text" +import { UserAvatar } from "@/components/ui/user-avatar" + +interface EntityInfo { + type: "performance" | "show" | "song" | "venue" | "tour" + slug: string + title: string + date?: string +} interface FeedItem { type: string @@ -14,8 +22,11 @@ interface FeedItem { user: { id: number username: string - avatar?: string + display_name?: string | null + avatar_bg_color?: string + avatar_text?: string | null } + entity?: EntityInfo | null } export function ActivityFeed() { @@ -44,6 +55,28 @@ export function ActivityFeed() { fetchFeed() }, []) + const getEntityLink = (entity: EntityInfo | null | undefined) => { + if (!entity) return null + const basePath = entity.type === "performance" ? "/performances" : + entity.type === "show" ? "/shows" : + entity.type === "song" ? "/songs" : + entity.type === "venue" ? "/venues" : "/tours" + return `${basePath}/${entity.slug}` + } + + const getEntityTypeLabel = (entity: EntityInfo | null | undefined) => { + if (!entity) return "something" + switch (entity.type) { + case "performance": return "a performance of" + case "show": return "the" + case "song": return "" + case "venue": return "" + default: return "" + } + } + + const displayName = (user: FeedItem["user"]) => user.display_name || user.username + if (loading) return
Loading activity...
return ( @@ -52,18 +85,55 @@ export function ActivityFeed() {
- {item.type === "review" && } - {item.type === "attendance" && } - {item.type === "post" && } +

- {item.user.username || "Anonymous"} - {item.type === "review" && " reviewed a show"} - {item.type === "attendance" && " attended a show"} + + {displayName(item.user)} + + {item.type === "review" && ( + <> + {" reviewed "} + {item.entity ? ( + + {getEntityTypeLabel(item.entity)} {item.entity.title} + {item.entity.date && ` (${new Date(item.entity.date).toLocaleDateString()})`} + + ) : ( + "a performance" + )} + + )} + {item.type === "attendance" && ( + <> + {" attended "} + {item.entity ? ( + + {item.entity.title} + + ) : ( + "a show" + )} + + )} {item.type === "post" && " posted in a group"}

- {item.type === "review" && ( + {item.type === "review" && item.data.blurb && (
""
@@ -72,7 +142,11 @@ export function ActivityFeed() {

{item.data.content}

)}

- {new Date(item.timestamp).toLocaleDateString()} + {new Date(item.timestamp).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric' + })}

diff --git a/frontend/components/social/entity-rating.tsx b/frontend/components/social/entity-rating.tsx index 6518357..4baf6c6 100644 --- a/frontend/components/social/entity-rating.tsx +++ b/frontend/components/social/entity-rating.tsx @@ -3,15 +3,28 @@ import { useState, useEffect } from "react" import { RatingInput, RatingBadge } from "@/components/ui/rating-input" import { getApiUrl } from "@/lib/api-config" +import { useAuth } from "@/contexts/auth-context" +import { Sparkles, TrendingUp } from "lucide-react" interface EntityRatingProps { entityType: "show" | "song" | "venue" | "tour" | "performance" entityId: number compact?: boolean + ratingCount?: number + rank?: number // Rank of this performance vs others + isHeady?: boolean // Is this a top-rated "heady" version } -export function EntityRating({ entityType, entityId, compact = false }: EntityRatingProps) { - const [userRating, setUserRating] = useState(0) +export function EntityRating({ + entityType, + entityId, + compact = false, + ratingCount, + rank, + isHeady = false +}: EntityRatingProps) { + const { user, token } = useAuth() + const [userRating, setUserRating] = useState(null) const [averageRating, setAverageRating] = useState(0) const [loading, setLoading] = useState(false) const [hasRated, setHasRated] = useState(false) @@ -22,11 +35,27 @@ export function EntityRating({ entityType, entityId, compact = false }: EntityRa .then(res => res.ok ? res.json() : 0) .then(data => setAverageRating(data || 0)) .catch(() => setAverageRating(0)) + + // Fetch user's rating if logged in + const storedToken = localStorage.getItem("token") + if (storedToken) { + fetch(`${getApiUrl()}/social/ratings/me?${entityType}_id=${entityId}`, { + headers: { Authorization: `Bearer ${storedToken}` } + }) + .then(res => res.ok ? res.json() : null) + .then(data => { + if (data?.score) { + setUserRating(data.score) + setHasRated(true) + } + }) + .catch(() => { }) + } }, [entityType, entityId]) const handleRate = async (score: number) => { - const token = localStorage.getItem("token") - if (!token) { + const storedToken = localStorage.getItem("token") + if (!storedToken) { alert("Please log in to rate.") return } @@ -40,7 +69,7 @@ export function EntityRating({ entityType, entityId, compact = false }: EntityRa method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}` + Authorization: `Bearer ${storedToken}` }, body: JSON.stringify(body) }) @@ -66,9 +95,20 @@ export function EntityRating({ entityType, entityId, compact = false }: EntityRa if (compact) { return (
+ {isHeady && ( + + + Heady + + )} {averageRating > 0 && ( )} + {rank && ( + + #{rank} + + )}
) } @@ -76,16 +116,33 @@ export function EntityRating({ entityType, entityId, compact = false }: EntityRa return (
- Your Rating - {averageRating > 0 && ( - - Community avg: {averageRating.toFixed(1)} - - )} +
+ Your Rating + {isHeady && ( + + + Heady Version + + )} +
+
+ {rank && ( + + + Ranked #{rank} + + )} + {averageRating > 0 && ( + + Community: {averageRating.toFixed(1)} + {ratingCount && ` (${ratingCount})`} + + )} +
@@ -95,9 +152,9 @@ export function EntityRating({ entityType, entityId, compact = false }: EntityRa Submitting...

)} - {hasRated && !loading && ( + {hasRated && !loading && userRating && (

- Rating saved! + Your rating: {userRating.toFixed(1)}/10

)}