- Review create mutation with auth, customer-only role check, self-review prevention, and ad ratingAvg/reviewCount recomputation - ReviewCard component: reviewer avatar, name, relative time in Macedonian, star rating, comment - Create review screen: interactive 5-star selector (Многу лошо–Отлично), comment field, auth gate, submit and navigate back - Ad detail: review section listing all reviews with reviewer names, overall rating summary with count, 'Напиши оценка' button - AdCard: rating stars shown next to title when ad has reviews All UI text in Macedonian. TypeScript clean, Convex functions deployed.
79 lines
2.2 KiB
TypeScript
79 lines
2.2 KiB
TypeScript
import { query, mutation } from "./_generated/server";
|
||
import { v } from "convex/values";
|
||
|
||
export const getByAd = query({
|
||
args: { adId: v.id("ads") },
|
||
handler: async (ctx, args) => {
|
||
return await ctx.db
|
||
.query("reviews")
|
||
.withIndex("by_ad", (q) => q.eq("adId", args.adId))
|
||
.order("desc")
|
||
.collect();
|
||
},
|
||
});
|
||
|
||
export const getByHandyman = query({
|
||
args: { handymanId: v.id("users") },
|
||
handler: async (ctx, args) => {
|
||
return await ctx.db
|
||
.query("reviews")
|
||
.withIndex("by_handyman", (q) => q.eq("handymanId", args.handymanId))
|
||
.order("desc")
|
||
.collect();
|
||
},
|
||
});
|
||
|
||
export const create = mutation({
|
||
args: {
|
||
token: v.string(),
|
||
adId: v.id("ads"),
|
||
rating: v.number(),
|
||
comment: v.optional(v.string()),
|
||
},
|
||
handler: async (ctx, args) => {
|
||
if (args.rating < 1 || args.rating > 5) {
|
||
throw new Error("Оцената мора да биде помеѓу 1 и 5");
|
||
}
|
||
|
||
const session = await ctx.db
|
||
.query("sessions")
|
||
.withIndex("by_token", (q) => q.eq("token", args.token))
|
||
.first();
|
||
if (!session) throw new Error("Неавторизиран");
|
||
|
||
const user = await ctx.db.get(session.userId);
|
||
if (!user) throw new Error("Корисникот не е пронајден");
|
||
if (user.role !== "customer") throw new Error("Само клиенти можат да пишуваат оцени");
|
||
|
||
const ad = await ctx.db.get(args.adId);
|
||
if (!ad) throw new Error("Огласот не е пронајден");
|
||
|
||
if (ad.handymanId === session.userId) {
|
||
throw new Error("Не можете да оцените свој оглас");
|
||
}
|
||
|
||
const reviewId = await ctx.db.insert("reviews", {
|
||
adId: args.adId,
|
||
customerId: session.userId,
|
||
handymanId: ad.handymanId,
|
||
rating: args.rating,
|
||
comment: args.comment,
|
||
createdAt: Date.now(),
|
||
});
|
||
|
||
const allReviews = await ctx.db
|
||
.query("reviews")
|
||
.withIndex("by_ad", (q) => q.eq("adId", args.adId))
|
||
.collect();
|
||
|
||
const totalRating = allReviews.reduce((sum, r) => sum + r.rating, 0);
|
||
const ratingAvg = totalRating / allReviews.length;
|
||
|
||
await ctx.db.patch(args.adId, {
|
||
ratingAvg,
|
||
reviewCount: allReviews.length,
|
||
});
|
||
|
||
return reviewId;
|
||
},
|
||
}); |