diff --git a/src/app/api/ratings/route.ts b/src/app/api/ratings/route.ts index c5099dbc..e328cbe8 100644 --- a/src/app/api/ratings/route.ts +++ b/src/app/api/ratings/route.ts @@ -4,6 +4,8 @@ import { publicClient } from "../../../../lib/rpc"; import { createServerClient, supabase } from "../../../../lib/supabase"; import { erc20Abi } from "../../../../lib/price"; +const MAX_COMMENT_LENGTH = 500; + function error(message: string, status = 400) { return NextResponse.json({ error: message }, { status }); } @@ -23,25 +25,55 @@ export async function GET(req: NextRequest) { return error("Supabase not configured", 500); } + const limit = Math.min(Number(req.nextUrl.searchParams.get("limit") ?? 20), 100); + const offset = Math.max(Number(req.nextUrl.searchParams.get("offset") ?? 0), 0); + const sid = Number(storylineId); + + // Fetch all ratings for global average/count, then slice for pagination + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data: allData, error: allError } = await (db.from("ratings") as any) + .select("rating") + .eq("storyline_id", sid); + + if (allError) { + return error(`Database error: ${allError.message}`, 500); + } + + const allRatings = allData ?? []; + const count = allRatings.length; + const average = + count > 0 + ? allRatings.reduce((sum: number, r: { rating: number }) => sum + r.rating, 0) / count + : 0; + + // Paginated query for full rating objects // eslint-disable-next-line @typescript-eslint/no-explicit-any const { data, error: dbError } = await (db.from("ratings") as any) .select("*") - .eq("storyline_id", Number(storylineId)); + .eq("storyline_id", sid) + .order("updated_at", { ascending: false }) + .range(offset, offset + limit - 1); if (dbError) { return error(`Database error: ${dbError.message}`, 500); } const ratings = data ?? []; - const average = - ratings.length > 0 - ? ratings.reduce( - (sum: number, r: { rating: number }) => sum + r.rating, - 0, - ) / ratings.length - : 0; - return NextResponse.json({ ratings, average, count: ratings.length }); + // Optionally look up the caller's own rating (may not be on current page) + const raterAddress = req.nextUrl.searchParams.get("raterAddress"); + let myRating: unknown = null; + if (raterAddress) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data: mine } = await (db.from("ratings") as any) + .select("*") + .eq("storyline_id", sid) + .eq("rater_address", raterAddress.toLowerCase()) + .single(); + myRating = mine ?? null; + } + + return NextResponse.json({ ratings, average, count, limit, offset, myRating }); } // --------------------------------------------------------------------------- @@ -77,6 +109,9 @@ export async function POST(req: NextRequest) { if (!address || !signature || !message) { return error("Missing address, signature, or message"); } + if (comment && comment.length > MAX_COMMENT_LENGTH) { + return error(`Comment must be ${MAX_COMMENT_LENGTH} characters or fewer`); + } // Validate signed message binds to this specific action (including comment) const boundComment = comment ?? ""; diff --git a/src/components/RatingWidget.tsx b/src/components/RatingWidget.tsx index dd6378ba..72cdca1d 100644 --- a/src/components/RatingWidget.tsx +++ b/src/components/RatingWidget.tsx @@ -21,6 +21,7 @@ interface RatingsResponse { ratings: RatingData[]; average: number; count: number; + myRating: RatingData | null; } interface RatingWidgetProps { @@ -60,9 +61,11 @@ export function RatingWidget({ storylineId, tokenAddress }: RatingWidgetProps) { // Fetch ratings const { data: ratingsData, refetch } = useQuery({ - queryKey: ["ratings", storylineId], + queryKey: ["ratings", storylineId, address], queryFn: async () => { - const res = await fetch(`/api/ratings?storylineId=${storylineId}`); + const params = new URLSearchParams({ storylineId: String(storylineId) }); + if (address) params.set("raterAddress", address); + const res = await fetch(`/api/ratings?${params}`); if (!res.ok) throw new Error("Failed to fetch ratings"); return res.json(); }, @@ -87,9 +90,7 @@ export function RatingWidget({ storylineId, tokenAddress }: RatingWidgetProps) { // Pre-fill existing rating or reset when wallet changes useEffect(() => { if (ratingsData && address) { - const existing = ratingsData.ratings.find( - (r) => r.rater_address === address.toLowerCase(), - ); + const existing = ratingsData.myRating; if (existing) { setSelectedRating(existing.rating); setComment(existing.comment ?? ""); @@ -187,6 +188,7 @@ export function RatingWidget({ storylineId, tokenAddress }: RatingWidgetProps) { value={comment} onChange={(e) => setComment(e.target.value)} disabled={submitting} + maxLength={500} rows={2} className="border-border bg-background text-foreground mt-2 w-full resize-none rounded border px-3 py-2 text-sm focus:border-accent focus:outline-none disabled:opacity-50" />