Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 44 additions & 9 deletions src/app/api/ratings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand All @@ -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 });
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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 ?? "";
Expand Down
12 changes: 7 additions & 5 deletions src/components/RatingWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface RatingsResponse {
ratings: RatingData[];
average: number;
count: number;
myRating: RatingData | null;
}

interface RatingWidgetProps {
Expand Down Expand Up @@ -60,9 +61,11 @@ export function RatingWidget({ storylineId, tokenAddress }: RatingWidgetProps) {

// Fetch ratings
const { data: ratingsData, refetch } = useQuery<RatingsResponse>({
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();
},
Expand All @@ -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 ?? "");
Expand Down Expand Up @@ -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"
/>
Expand Down
Loading